xiiregexbuilder

FPGA-Accelerated Regular Expression Matching Engine

src/emitter.cpp (46608B)


   1#include "emitter.h"
   2#include <fstream>
   3#include <iostream>
   4#include <iomanip>
   5#include <sstream>
   6#include <algorithm>
   7#include <set>
   8#include <map>
   9#include <system_error>
  10#include <filesystem>
  11
  12// =============================================================================
  13// Emitter::emit -  orchestration
  14// =============================================================================
  15void Emitter::emit(const std::vector<std::unique_ptr<NFA>> &nfas,
  16                   const std::string &outputDirStr,
  17                   const std::vector<std::string> &testStrings,
  18                   const std::vector<std::string> &expectedMatches,
  19                   bool isPII)
  20{
  21    if (nfas.empty())
  22    {
  23        std::cout << "No valid NFAs were provided; skipping Verilog emission." << std::endl;
  24        return;
  25    }
  26
  27    std::filesystem::path outputDir(outputDirStr);
  28    try
  29    {
  30        std::filesystem::create_directories(outputDir);
  31    }
  32    catch (const std::filesystem::filesystem_error &e)
  33    {
  34        throw std::system_error(e.code(), "Failed to create output directory: " + outputDir.string());
  35    }
  36
  37    for (const auto &nfa : nfas)
  38    {
  39        emitNFAModule(*nfa, outputDir, isPII);
  40    }
  41
  42    emitTopModule(nfas, outputDir);
  43    if (isPII)
  44    {
  45        emitPIIUART(outputDir);
  46        emitPIIUARTTx(outputDir);
  47        emitPIITopFPGA(nfas, outputDir);
  48    }
  49    else
  50    {
  51        emitUARTRX(outputDir);
  52        emitUARTTX(outputDir);
  53        emitFIFO(outputDir);
  54        emitTopFPGA(nfas, outputDir);
  55    }
  56
  57    emitConstraints(nfas, outputDir);
  58    emitTestbench(nfas, outputDir, testStrings, expectedMatches);
  59}
  60
  61// =============================================================================
  62// emitNFAModule -  one file per regex
  63// =============================================================================
  64void Emitter::emitNFAModule(const NFA &nfa, const std::filesystem::path &outputDir, bool isPII)
  65{
  66    auto filePath = outputDir / ("nfa_" + std::to_string(nfa.regexIndex) + ".v");
  67    std::ofstream out;
  68    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
  69
  70    try
  71    {
  72        out.open(filePath);
  73    }
  74    catch (const std::ios_base::failure &e)
  75    {
  76        throw std::system_error(errno, std::generic_category(), "Could not open " + filePath.string() + " for writing");
  77    }
  78
  79    int numStates = static_cast<int>(nfa.states.size());
  80    if (numStates == 0)
  81        return;
  82
  83    std::map<int, int> globalToLocal;
  84    int localIdx = 0;
  85
  86    // Ensure start state is always local state 0
  87    globalToLocal[nfa.startStateId] = localIdx++;
  88
  89    // Sort other state IDs for deterministic output
  90    std::set<int> otherIds;
  91    for (const auto &pair : nfa.states)
  92    {
  93        if (pair.first != nfa.startStateId)
  94            otherIds.insert(pair.first);
  95    }
  96    for (int id : otherIds)
  97    {
  98        globalToLocal[id] = localIdx++;
  99    }
 100
 101    out << "`timescale 1ns / 1ps\n\n";
 102    out << "// NFA for regex index " << nfa.regexIndex << "\n";
 103    out << "module nfa_" << nfa.regexIndex << " (\n"
 104        << "    input  wire       clk,\n"
 105        << "    input  wire       en,\n"
 106        << "    input  wire       rst,\n"
 107        << "    input  wire       start,\n"
 108        << "    input  wire       end_of_str,\n"
 109        << "    input  wire [7:0] char_in,\n"
 110        << "    output " << (isPII ? "wire       " : "reg        ") << "match,\n"
 111        << "    output wire       active\n"
 112        << ");\n\n";
 113
 114    out << "    // One-hot state register\n"
 115        << "    reg [" << numStates - 1 << ":0] state_reg;\n"
 116        << "    wire [" << numStates - 1 << ":0] next_state;\n\n";
 117
 118    std::map<int, std::vector<std::pair<int, unsigned char>>> invertedTransitions;
 119    for (const auto &[srcGlobalId, srcState] : nfa.states)
 120    {
 121        for (const auto &[c, dstIds] : srcState.transitions)
 122        {
 123            for (int dstGlobalId : dstIds)
 124            {
 125                invertedTransitions[dstGlobalId].push_back({srcGlobalId, c});
 126            }
 127        }
 128    }
 129
 130    for (const auto &[globalId, localId] : globalToLocal)
 131    {
 132        out << "    assign next_state[" << localId << "] = ";
 133
 134        std::vector<std::string> terms;
 135
 136        if (isPII && localId == 0)
 137        {
 138            terms.push_back("1'b1");
 139        }
 140
 141        if (auto it = invertedTransitions.find(globalId); it != invertedTransitions.end())
 142        {
 143            std::map<int, std::vector<unsigned char>> charsBySrc;
 144            for (const auto &[srcGlobalId, c] : it->second)
 145            {
 146                charsBySrc[globalToLocal.at(srcGlobalId)].push_back(c);
 147            }
 148
 149            // Magnitude comparators for contiguous wildcard ranges
 150            for (auto &[srcLocal, chars] : charsBySrc)
 151            {
 152                std::sort(chars.begin(), chars.end());
 153                size_t i = 0;
 154                while (i < chars.size())
 155                {
 156                    size_t j = i;
 157                    while (j + 1 < chars.size() && chars[j + 1] == chars[j] + 1)
 158                    {
 159                        j++;
 160                    }
 161                    if (j - i >= 3)
 162                    {
 163                        terms.push_back("(state_reg[" + std::to_string(srcLocal) +
 164                                        "] && (char_in >= 8'd" + std::to_string(chars[i]) +
 165                                        ") && (char_in <= 8'd" + std::to_string(chars[j]) + "))");
 166                    }
 167                    else
 168                    {
 169                        for (size_t k = i; k <= j; ++k)
 170                        {
 171                            terms.push_back("(state_reg[" + std::to_string(srcLocal) +
 172                                            "] && (char_in == 8'd" + std::to_string(chars[k]) + "))");
 173                        }
 174                    }
 175                    i = j + 1;
 176                }
 177            }
 178        }
 179
 180        if (terms.empty())
 181        {
 182            out << "1'b0;\n";
 183        }
 184        else
 185        {
 186            out << terms[0];
 187            for (size_t t = 1; t < terms.size(); ++t)
 188            {
 189                out << " | " << terms[t];
 190            }
 191            out << ";\n";
 192        }
 193    }
 194
 195    out << "\n    always @(posedge clk) begin\n"
 196        << "        if (rst || start) begin\n"
 197        << "            // Reset to start state (one-hot)\n"
 198        << "            state_reg <= 1 << " << globalToLocal.at(nfa.startStateId) << ";\n"
 199        << "        end else if (en) begin\n"
 200        << "            state_reg <= next_state;\n"
 201        << "        end\n"
 202        << "    end\n\n";
 203
 204    if (isPII)
 205    {
 206        out << "    // Match logic: asserted immediately on accept state (combinational)\n"
 207            << "    assign match = ";
 208    }
 209    else
 210    {
 211        out << "    // Match logic: asserted on cycle following end_of_str\n"
 212            << "    always @(posedge clk) begin\n"
 213            << "        if (rst || start) begin\n"
 214            << "            match <= 1'b0;\n"
 215            << "        end else if (en) begin\n"
 216            << "            if (end_of_str) begin\n"
 217            << "                match <= ";
 218    }
 219
 220    std::vector<std::string> acceptTerms;
 221    for (const auto &[id, state] : nfa.states)
 222    {
 223        if (state.isAccept)
 224        {
 225            acceptTerms.push_back("state_reg[" + std::to_string(globalToLocal.at(id)) + "]");
 226        }
 227    }
 228
 229    if (acceptTerms.empty())
 230    {
 231        out << "1'b0";
 232    }
 233    else if (acceptTerms.size() == 1)
 234    {
 235        out << acceptTerms[0];
 236    }
 237    else
 238    {
 239        out << "(|{";
 240        for (size_t t = 0; t < acceptTerms.size(); ++t)
 241        {
 242            out << acceptTerms[t] << (t < acceptTerms.size() - 1 ? ", " : "");
 243        }
 244        out << "})";
 245    }
 246
 247    if (isPII)
 248    {
 249        out << ";\n\n";
 250    }
 251    else
 252    {
 253        out << ";\n"
 254            << "            end else begin\n"
 255            << "                match <= 1'b0;\n"
 256            << "            end\n"
 257            << "        end\n"
 258            << "    end\n\n";
 259    }
 260    out << "    // Active logic: high if any state other than state 0 is active\n";
 261    if (numStates > 1)
 262    {
 263        out << "    assign active = |state_reg[" << numStates - 1 << ":1];\n\n";
 264    }
 265    else
 266    {
 267        out << "    assign active = 1'b0;\n\n";
 268    }
 269    out << "endmodule\n";
 270}
 271
 272// =============================================================================
 273// emitTopModule -  top.v wrapper
 274// =============================================================================
 275void Emitter::emitTopModule(const std::vector<std::unique_ptr<NFA>> &nfas, const std::filesystem::path &outputDir)
 276{
 277    auto filePath = outputDir / "top.v";
 278    std::ofstream out;
 279    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 280
 281    try
 282    {
 283        out.open(filePath);
 284    }
 285    catch (const std::ios_base::failure &e)
 286    {
 287        throw std::system_error(errno, std::generic_category(), "Could not open " + filePath.string() + " for writing");
 288    }
 289
 290    out << "`timescale 1ns / 1ps\n\n";
 291    out << "module top (\n"
 292        << "    input  wire       clk,\n"
 293        << "    input  wire       en,\n"
 294        << "    input  wire       rst,\n"
 295        << "    input  wire       start,\n"
 296        << "    input  wire       end_of_str,\n"
 297        << "    input  wire [7:0] char_in,\n"
 298        << "    output wire [" << (nfas.size() > 0 ? nfas.size() - 1 : 0) << ":0] match_bus,\n"
 299        << "    output wire [" << (nfas.size() > 0 ? nfas.size() - 1 : 0) << ":0] active_bus\n"
 300        << ");\n\n";
 301
 302    for (size_t i = 0; i < nfas.size(); ++i)
 303    {
 304        out << "    nfa_" << nfas[i]->regexIndex << " inst_" << nfas[i]->regexIndex << " (\n"
 305            << "        .clk(clk), .en(en), .rst(rst), .start(start), .end_of_str(end_of_str), .char_in(char_in), .match(match_bus[" << i << "]), .active(active_bus[" << i << "])\n"
 306            << "    );\n\n";
 307    }
 308    out << "endmodule\n";
 309}
 310
 311// =============================================================================
 312// emitTestbench -  tb_top.v
 313// =============================================================================
 314void Emitter::emitTestbench(const std::vector<std::unique_ptr<NFA>> &nfas,
 315                            const std::filesystem::path &outputDir,
 316                            const std::vector<std::string> &testStrings,
 317                            const std::vector<std::string> &expectedMatches)
 318{
 319    auto filePath = outputDir / "tb_top.v";
 320    std::ofstream out;
 321    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 322
 323    try
 324    {
 325        out.open(filePath);
 326    }
 327    catch (const std::ios_base::failure &e)
 328    {
 329        throw std::system_error(errno, std::generic_category(), "Could not open " + filePath.string() + " for writing");
 330    }
 331
 332    size_t numNFAs = nfas.size();
 333
 334    out << "`timescale 1ns / 1ps\n\n"
 335        << "module tb_top;\n"
 336        << "    reg clk, en, rst, start, end_of_str;\n"
 337        << "    reg [7:0] char_in;\n"
 338        << "    wire [" << (numNFAs > 0 ? numNFAs - 1 : 0) << ":0] match_bus;\n"
 339        << "    wire [" << (numNFAs > 0 ? numNFAs - 1 : 0) << ":0] active_bus;\n\n"
 340        << "    top uut (.clk(clk), .en(en), .rst(rst), .start(start), .end_of_str(end_of_str), .char_in(char_in), .match_bus(match_bus), .active_bus(active_bus));\n\n"
 341        << "    always #5 clk = ~clk;\n\n"
 342        << "    initial begin\n"
 343        << "        // synthesis translate_off\n"
 344        << "        `ifndef SYNTHESIS\n"
 345        << "        $dumpfile(\"dump.vcd\");\n"
 346        << "        $dumpvars(0, tb_top);\n"
 347        << "        `endif\n"
 348        << "        // synthesis translate_on\n\n"
 349        << "        clk = 0; en = 1; rst = 1; start = 0; end_of_str = 0; char_in = 0;\n"
 350        << "        #20 rst = 0; #10;\n\n";
 351
 352    for (size_t i = 0; i < testStrings.size(); ++i)
 353    {
 354        const std::string &s = testStrings[i];
 355        out << "        // Test case " << i << ": \"" << s << "\"\n"
 356            << "        start = 1; #10 start = 0;\n";
 357
 358        for (char c : s)
 359        {
 360            out << "        char_in = 8'h" << std::hex << std::setw(2) << std::setfill('0')
 361                << static_cast<int>(static_cast<unsigned char>(c)) << std::dec << "; #10;\n";
 362        }
 363
 364        out << "        end_of_str = 1; #10;\n";
 365        out << "        // Match output is valid on the cycle immediately following end_of_str assertion\n";
 366
 367        if (i < expectedMatches.size())
 368        {
 369            if (expectedMatches[i].length() != numNFAs)
 370            {
 371                throw std::runtime_error(
 372                    "Testbench generation error: expected_matches[" + std::to_string(i) +
 373                    "] has length " + std::to_string(expectedMatches[i].length()) +
 374                    " but " + std::to_string(numNFAs) + " NFAs exist.");
 375            }
 376            out << "        if (match_bus === " << numNFAs << "'b" << expectedMatches[i] << ") begin\n"
 377                << "            $display(\"PASS: Test case " << i << " ('" << s << "') matches expected mask " << expectedMatches[i] << "\");\n"
 378                << "        end else begin\n"
 379                << "            $display(\"FAIL: Test case " << i << " ('" << s << "') expected " << expectedMatches[i] << ", got %b\", match_bus);\n"
 380                << "        end\n";
 381        }
 382        else
 383        {
 384            out << "        $display(\"INFO: Result for '" << s << "': %b\", match_bus);\n";
 385        }
 386        out << "        end_of_str = 0; #10;\n\n";
 387    }
 388
 389    out << "        $display(\"All tests completed.\");\n"
 390        << "        #100; $finish;\n    end\nendmodule\n";
 391}
 392
 393// =============================================================================
 394// emitUARTRX -  uart_rx.v
 395// =============================================================================
 396void Emitter::emitUARTRX(const std::filesystem::path &outputDir)
 397{
 398    auto filePath = outputDir / "uart_rx.v";
 399    std::ofstream out(filePath);
 400    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 401
 402    out << "`timescale 1ns / 1ps\n\n";
 403    out << R"(
 404module uart_rx #(
 405    parameter CLKS_PER_BIT = 868 // 100 MHz / 115200 Baud
 406)(
 407    input  wire       clk,
 408    input  wire       rx,
 409    output reg  [7:0] rx_data,
 410    output reg        rx_ready
 411);
 412    localparam IDLE = 2'b00, START_BIT = 2'b01, DATA_BITS = 2'b10, STOP_BIT = 2'b11;
 413    reg [1:0] state = IDLE;
 414    reg [9:0] clk_count = 0;
 415    reg [2:0] bit_idx = 0;
 416
 417    always @(posedge clk) begin
 418        case (state)
 419            IDLE: begin
 420                rx_ready <= 1'b0;
 421                clk_count <= 0;
 422                bit_idx <= 0;
 423                if (rx == 1'b0) state <= START_BIT;
 424            end
 425            START_BIT: begin
 426                if (clk_count == (CLKS_PER_BIT-1)/2) begin
 427                    if (rx == 1'b0) begin
 428                        clk_count <= 0;
 429                        state <= DATA_BITS;
 430                    end else state <= IDLE;
 431                end else clk_count <= clk_count + 1;
 432            end
 433            DATA_BITS: begin
 434                if (clk_count < CLKS_PER_BIT-1) begin
 435                    clk_count <= clk_count + 1;
 436                end else begin
 437                    clk_count <= 0;
 438                    rx_data[bit_idx] <= rx;
 439                    if (bit_idx < 7) bit_idx <= bit_idx + 1;
 440                    else state <= STOP_BIT;
 441                end
 442            end
 443            STOP_BIT: begin
 444                if (clk_count < CLKS_PER_BIT-1) begin
 445                    clk_count <= clk_count + 1;
 446                end else begin
 447                    if (rx == 1'b1) rx_ready <= 1'b1;
 448                    state <= IDLE;
 449                end
 450            end
 451        endcase
 452    end
 453endmodule
 454)";
 455}
 456
 457// =============================================================================
 458// emitUARTTX - uart_tx.v
 459// =============================================================================
 460void Emitter::emitUARTTX(const std::filesystem::path &outputDir)
 461{
 462    auto filePath = outputDir / "uart_tx.v";
 463    std::ofstream out(filePath);
 464    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 465
 466    out << "`timescale 1ns / 1ps\n\n";
 467    out << R"(
 468module uart_tx #(
 469    parameter CLKS_PER_BIT = 868   // 100 MHz / 115200 baud
 470)(
 471    input  wire       clk,
 472    input  wire       rst,
 473    input  wire [7:0] tx_data,
 474    input  wire       tx_start,
 475    output reg        tx_busy,
 476    output reg        tx
 477);
 478    localparam S_IDLE      = 3'd0;
 479    localparam S_START_BIT = 3'd1;
 480    localparam S_DATA_BITS = 3'd2;
 481    localparam S_STOP_BIT  = 3'd3;
 482
 483    reg [2:0]  state    = S_IDLE;
 484    reg [9:0]  clk_cnt  = 10'd0;
 485    reg [2:0]  bit_idx  = 3'd0;
 486    reg [7:0]  tx_shift = 8'd0;
 487
 488    always @(posedge clk) begin
 489        if (rst) begin
 490            state    <= S_IDLE;
 491            tx       <= 1'b1;
 492            tx_busy  <= 1'b0;
 493            clk_cnt  <= 10'd0;
 494            bit_idx  <= 3'd0;
 495            tx_shift <= 8'd0;
 496        end else begin
 497            case (state)
 498                S_IDLE: begin
 499                    tx      <= 1'b1;
 500                    tx_busy <= 1'b0;
 501                    clk_cnt <= 10'd0;
 502                    bit_idx <= 3'd0;
 503                    if (tx_start) begin
 504                        tx_shift <= tx_data;
 505                        tx_busy  <= 1'b1;
 506                        state    <= S_START_BIT;
 507                    end
 508                end
 509                S_START_BIT: begin
 510                    tx <= 1'b0;
 511                    if (clk_cnt < CLKS_PER_BIT - 1)
 512                        clk_cnt <= clk_cnt + 1;
 513                    else begin
 514                        clk_cnt <= 10'd0;
 515                        state   <= S_DATA_BITS;
 516                    end
 517                end
 518                S_DATA_BITS: begin
 519                    tx <= tx_shift[0];
 520                    if (clk_cnt < CLKS_PER_BIT - 1)
 521                        clk_cnt <= clk_cnt + 1;
 522                    else begin
 523                        clk_cnt  <= 10'd0;
 524                        tx_shift <= tx_shift >> 1;
 525                        if (bit_idx < 7)
 526                            bit_idx <= bit_idx + 1;
 527                        else begin
 528                            bit_idx <= 3'd0;
 529                            state   <= S_STOP_BIT;
 530                        end
 531                    end
 532                end
 533                S_STOP_BIT: begin
 534                    tx <= 1'b1;
 535                    if (clk_cnt < CLKS_PER_BIT - 1)
 536                        clk_cnt <= clk_cnt + 1;
 537                    else begin
 538                        clk_cnt <= 10'd0;
 539                        state   <= S_IDLE;
 540                    end
 541                end
 542                default: state <= S_IDLE;
 543            endcase
 544        end
 545    end
 546endmodule
 547)";
 548}
 549
 550// =============================================================================
 551// emitFIFO - uart_rx_fifo.v
 552// =============================================================================
 553void Emitter::emitFIFO(const std::filesystem::path &outputDir)
 554{
 555    auto filePath = outputDir / "uart_rx_fifo.v";
 556    std::ofstream out(filePath);
 557    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 558
 559    out << "`timescale 1ns / 1ps\n\n";
 560    out << R"(
 561module uart_rx_fifo #(
 562    parameter DEPTH_LOG2 = 4        // depth = 2^DEPTH_LOG2 = 16
 563)(
 564    input  wire       clk,
 565    input  wire       rst,
 566    input  wire [7:0] wr_data,
 567    input  wire       wr_en,
 568    output wire       full,
 569    output wire [7:0] rd_data,
 570    input  wire       rd_en,
 571    output wire       empty
 572);
 573    localparam DEPTH = 1 << DEPTH_LOG2;
 574
 575    reg [7:0] mem [0:DEPTH-1];
 576    reg [DEPTH_LOG2-1:0] wr_ptr = {DEPTH_LOG2{1'b0}};
 577    reg [DEPTH_LOG2-1:0] rd_ptr = {DEPTH_LOG2{1'b0}};
 578    reg [DEPTH_LOG2  :0] count  = {(DEPTH_LOG2+1){1'b0}};
 579
 580    assign full    = (count == DEPTH[DEPTH_LOG2:0]);
 581    assign empty   = (count == {(DEPTH_LOG2+1){1'b0}});
 582    assign rd_data = mem[rd_ptr];
 583
 584    always @(posedge clk) begin
 585        if (rst) begin
 586            wr_ptr <= {DEPTH_LOG2{1'b0}};
 587            rd_ptr <= {DEPTH_LOG2{1'b0}};
 588            count  <= {(DEPTH_LOG2+1){1'b0}};
 589        end else begin
 590            if (wr_en && !full && rd_en && !empty) begin
 591                mem[wr_ptr] <= wr_data;
 592                wr_ptr      <= wr_ptr + 1;
 593                rd_ptr      <= rd_ptr + 1;
 594            end else if (wr_en && !full) begin
 595                mem[wr_ptr] <= wr_data;
 596                wr_ptr      <= wr_ptr + 1;
 597                count       <= count + 1;
 598            end else if (rd_en && !empty) begin
 599                rd_ptr <= rd_ptr + 1;
 600                count  <= count - 1;
 601            end
 602        end
 603    end
 604endmodule
 605)";
 606}
 607
 608// =============================================================================
 609// emitTopFPGA - top_fpga.v
 610// =============================================================================
 611void Emitter::emitTopFPGA(const std::vector<std::unique_ptr<NFA>> &nfas, const std::filesystem::path &outputDir)
 612{
 613    auto filePath = outputDir / "top_fpga.v";
 614    std::ofstream out(filePath);
 615    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 616
 617    const size_t numNFAs = nfas.size();
 618    const std::string busHigh = std::to_string(numNFAs > 0 ? numNFAs - 1 : 0);
 619
 620    // File Header
 621    out << "`timescale 1ns / 1ps\n\n"
 622        << "// =============================================================================\n"
 623        << "// top_fpga.v -  FPGA Top-Level\n"
 624        << "// Regex count: " << numNFAs << "\n"
 625        << "//\n"
 626        << "// Architecture:\n"
 627        << "//   uart_rx → uart_rx_fifo → Control FSM → top (NFA engine)\n"
 628        << "//                                         → uart_tx → host PC\n"
 629        << "//\n"
 630        << "// UART response packet (one line per newline received from host):\n"
 631        << "//   \"MATCH=<" << numNFAs << "-bit binary> BYTES=<8 hex> HITS=<4 hex per regex,comma-sep>\\r\\n\"\n"
 632        << "//\n"
 633        << "// Send '?' (0x3F) to query counters without feeding the NFA.\n"
 634        << "// =============================================================================\n\n";
 635
 636    // Module Declaration
 637    out << "module top_fpga #(\n"
 638        << "    parameter NUM_REGEX    = " << numNFAs << ",\n"
 639        << "    parameter CLKS_PER_BIT = 868   // 100 MHz / 115200 baud\n"
 640        << ")(\n"
 641        << "    input  wire clk,\n"
 642        << "    input  wire rst_btn,\n"
 643        << "    input  wire uart_rx_pin,\n"
 644        << "    output wire uart_tx_pin,\n"
 645        << "    output reg  [" << busHigh << ":0] match_leds\n"
 646        << ");\n\n";
 647
 648    // uart_rx
 649    out << "    // UART RX\n"
 650        << "    wire [7:0] rx_data;\n"
 651        << "    wire       rx_ready;\n\n"
 652        << "    uart_rx #(.CLKS_PER_BIT(CLKS_PER_BIT)) uart_rx_inst (\n"
 653        << "        .clk     (clk),\n"
 654        << "        .rx      (uart_rx_pin),\n"
 655        << "        .rx_data (rx_data),\n"
 656        << "        .rx_ready(rx_ready)\n"
 657        << "    );\n\n";
 658
 659    // FIFO
 660    out << "    // Input FIFO\n"
 661        << "    wire [7:0] fifo_rd_data;\n"
 662        << "    wire       fifo_empty;\n"
 663        << "    wire       fifo_full;\n"
 664        << "    reg        fifo_rd_en = 1'b0;\n\n"
 665        << "    uart_rx_fifo #(.DEPTH_LOG2(4)) rx_fifo (\n"
 666        << "        .clk    (clk),\n"
 667        << "        .rst    (rst_btn),\n"
 668        << "        .wr_data(rx_data),\n"
 669        << "        .wr_en  (rx_ready && !fifo_full),\n"
 670        << "        .full   (fifo_full),\n"
 671        << "        .rd_data(fifo_rd_data),\n"
 672        << "        .rd_en  (fifo_rd_en),\n"
 673        << "        .empty  (fifo_empty)\n"
 674        << "    );\n\n";
 675
 676    // NFA engine
 677    out << "    // NFA Engine\n"
 678        << "    reg                  nfa_start      = 1'b1;\n"
 679        << "    reg                  nfa_end_of_str = 1'b0;\n"
 680        << "    reg  [7:0]           nfa_char_in    = 8'h00;\n"
 681        << "    reg                  nfa_en         = 1'b0;\n"
 682        << "    wire [" << busHigh << ":0] match_bus;\n\n"
 683        << "    top regex_engine (\n"
 684        << "        .clk        (clk),\n"
 685        << "        .en         (nfa_en),\n"
 686        << "        .rst        (rst_btn),\n"
 687        << "        .start      (nfa_start),\n"
 688        << "        .end_of_str (nfa_end_of_str),\n"
 689        << "        .char_in    (nfa_char_in),\n"
 690        << "        .match_bus  (match_bus)\n"
 691        << "    );\n\n";
 692
 693    // Hardware Counters
 694    out << "    // Hardware Counters\n"
 695        << "    reg [31:0] byte_count = 32'd0;\n"
 696        << "    // One 16-bit hit counter per regex\n"
 697        << "    reg [15:0] match_count [0:15];\n"
 698        << "    integer ci;\n"
 699        << "    initial begin\n"
 700        << "        for (ci = 0; ci < 16; ci = ci + 1)\n"
 701        << "            match_count[ci] = 16'd0;\n"
 702        << "    end\n\n";
 703
 704    // uart_tx + TX drain sub-FSM
 705    out << "    // UART TX & Drain Sub-FSM\n"
 706        << "    localparam TX_BUF_LEN = 128;\n"
 707        << "    reg [7:0] tx_buf [0:TX_BUF_LEN-1];\n"
 708        << "    reg [6:0] tx_len  = 7'd0;\n"
 709        << "    reg [6:0] tx_ptr  = 7'd0;\n"
 710        << "    reg       tx_send = 1'b0;  // pulse: start draining tx_buf\n\n"
 711        << "    wire      tx_busy;\n"
 712        << "    reg       tx_start_r = 1'b0;\n"
 713        << "    reg [7:0] tx_data_r  = 8'd0;\n\n"
 714        << "    uart_tx #(.CLKS_PER_BIT(CLKS_PER_BIT)) uart_tx_inst (\n"
 715        << "        .clk     (clk),\n"
 716        << "        .rst     (rst_btn),\n"
 717        << "        .tx_data (tx_data_r),\n"
 718        << "        .tx_start(tx_start_r),\n"
 719        << "        .tx_busy (tx_busy),\n"
 720        << "        .tx      (uart_tx_pin)\n"
 721        << "    );\n\n"
 722        << "    // hex nibble → ASCII character\n"
 723        << "    function [7:0] hex_char;\n"
 724        << "        input [3:0] nibble;\n"
 725        << "        begin\n"
 726        << "            hex_char = (nibble < 4'd10) ? (8'd48 + nibble) : (8'd55 + nibble);\n"
 727        << "        end\n"
 728        << "    endfunction\n\n"
 729        << "    localparam TX_IDLE = 2'd0, TX_LOAD = 2'd1, TX_WAIT = 2'd2, TX_NEXT = 2'd3;\n"
 730        << "    reg [1:0] tx_state = TX_IDLE;\n\n"
 731        << "    always @(posedge clk) begin\n"
 732        << "        tx_start_r <= 1'b0;\n"
 733        << "        if (rst_btn) begin\n"
 734        << "            tx_state <= TX_IDLE;\n"
 735        << "            tx_ptr   <= 7'd0;\n"
 736        << "        end else begin\n"
 737        << "            case (tx_state)\n"
 738        << "                TX_IDLE: if (tx_send) begin tx_ptr <= 7'd0; tx_state <= TX_LOAD; end\n"
 739        << "                TX_LOAD: if (!tx_busy) begin\n"
 740        << "                    tx_data_r  <= tx_buf[tx_ptr];\n"
 741        << "                    tx_start_r <= 1'b1;\n"
 742        << "                    tx_state   <= TX_WAIT;\n"
 743        << "                end\n"
 744        << "                TX_WAIT: if (tx_busy)  tx_state <= TX_NEXT;\n"
 745        << "                TX_NEXT: if (!tx_busy) begin\n"
 746        << "                    if (tx_ptr + 1 < tx_len) begin\n"
 747        << "                        tx_ptr   <= tx_ptr + 1;\n"
 748        << "                        tx_state <= TX_LOAD;\n"
 749        << "                    end else\n"
 750        << "                        tx_state <= TX_IDLE;\n"
 751        << "                end\n"
 752        << "            endcase\n"
 753        << "        end\n"
 754        << "    end\n\n";
 755
 756    // build_response Task
 757    // The task is parameterised on the actual regex count at code-gen time
 758    out << "    // build_response Task\n"
 759        << "    // Fills tx_buf with the ASCII response packet in one clock cycle.\n"
 760        << "    // Format: \"MATCH=<bits> BYTES=<8hex> HITS=<4hex per regex,csv>\\r\\n\"\n"
 761        << "    reg [6:0]  bp;         // build pointer (local to task scope)\n"
 762        << "    reg [15:0] tmp16;\n"
 763        << "    integer    ri;\n\n"
 764        << "    task build_response;\n"
 765        << "        input [" << busHigh << ":0] mbits;\n"
 766        << "        input [31:0]          bcount;\n"
 767        << "        integer k;\n"
 768        << "        begin : build_task\n"
 769        << "            integer p;\n"
 770        << "            p = 0;\n"
 771        << "            // \"MATCH=\"\n"
 772        << "            tx_buf[p]=8'h4D; p=p+1;  // M\n"
 773        << "            tx_buf[p]=8'h41; p=p+1;  // A\n"
 774        << "            tx_buf[p]=8'h54; p=p+1;  // T\n"
 775        << "            tx_buf[p]=8'h43; p=p+1;  // C\n"
 776        << "            tx_buf[p]=8'h48; p=p+1;  // H\n"
 777        << "            tx_buf[p]=8'h3D; p=p+1;  // =\n"
 778        << "            // match bits, MSB first\n";
 779
 780    // Unroll the match-bit loop at C++ code-gen time for exact bit count
 781    for (int b = static_cast<int>(numNFAs) - 1; b >= 0; --b)
 782    {
 783        out << "            tx_buf[p] = (mbits[" << b << "]) ? 8'h31 : 8'h30; p=p+1;\n";
 784    }
 785
 786    out << "            // \" BYTES=\"\n"
 787        << "            tx_buf[p]=8'h20; p=p+1;\n"
 788        << "            tx_buf[p]=8'h42; p=p+1;  // B\n"
 789        << "            tx_buf[p]=8'h59; p=p+1;  // Y\n"
 790        << "            tx_buf[p]=8'h54; p=p+1;  // T\n"
 791        << "            tx_buf[p]=8'h45; p=p+1;  // E\n"
 792        << "            tx_buf[p]=8'h53; p=p+1;  // S\n"
 793        << "            tx_buf[p]=8'h3D; p=p+1;  // =\n"
 794        << "            tx_buf[p]=hex_char(bcount[31:28]); p=p+1;\n"
 795        << "            tx_buf[p]=hex_char(bcount[27:24]); p=p+1;\n"
 796        << "            tx_buf[p]=hex_char(bcount[23:20]); p=p+1;\n"
 797        << "            tx_buf[p]=hex_char(bcount[19:16]); p=p+1;\n"
 798        << "            tx_buf[p]=hex_char(bcount[15:12]); p=p+1;\n"
 799        << "            tx_buf[p]=hex_char(bcount[11: 8]); p=p+1;\n"
 800        << "            tx_buf[p]=hex_char(bcount[ 7: 4]); p=p+1;\n"
 801        << "            tx_buf[p]=hex_char(bcount[ 3: 0]); p=p+1;\n"
 802        << "            // \" HITS=\"\n"
 803        << "            tx_buf[p]=8'h20; p=p+1;\n"
 804        << "            tx_buf[p]=8'h48; p=p+1;  // H\n"
 805        << "            tx_buf[p]=8'h49; p=p+1;  // I\n"
 806        << "            tx_buf[p]=8'h54; p=p+1;  // T\n"
 807        << "            tx_buf[p]=8'h53; p=p+1;  // S\n"
 808        << "            tx_buf[p]=8'h3D; p=p+1;  // =\n";
 809
 810    // Unroll the per-regex hits loop at C++ code-gen time
 811    for (size_t k = 0; k < numNFAs; ++k)
 812    {
 813        out << "            tmp16 = match_count[" << k << "];\n"
 814            << "            tx_buf[p]=hex_char(tmp16[15:12]); p=p+1;\n"
 815            << "            tx_buf[p]=hex_char(tmp16[11: 8]); p=p+1;\n"
 816            << "            tx_buf[p]=hex_char(tmp16[ 7: 4]); p=p+1;\n"
 817            << "            tx_buf[p]=hex_char(tmp16[ 3: 0]); p=p+1;\n";
 818        if (k < numNFAs - 1)
 819            out << "            tx_buf[p]=8'h2C; p=p+1;  // ','\n";
 820    }
 821
 822    out << "            tx_buf[p]=8'h0D; p=p+1;  // CR\n"
 823        << "            tx_buf[p]=8'h0A; p=p+1;  // LF\n"
 824        << "            tx_len = p[6:0];\n"
 825        << "        end\n"
 826        << "    endtask\n\n";
 827
 828    // Main Control FSM
 829    out << "    // Main Control FSM\n"
 830        << "    localparam S_IDLE      = 4'd0;\n"
 831        << "    localparam S_FETCH     = 4'd1;\n"
 832        << "    localparam S_DECODE    = 4'd2;\n"
 833        << "    localparam S_CHAR_LOAD = 4'd3;\n"
 834        << "    localparam S_CHAR_STEP = 4'd4;\n"
 835        << "    localparam S_EOL_END   = 4'd5;\n"
 836        << "    localparam S_EOL_MATCH = 4'd6;\n"
 837        << "    localparam S_EOL_LATCH = 4'd7;\n"
 838        << "    localparam S_TX_ARM    = 4'd8;\n"
 839        << "    localparam S_TX_WAIT   = 4'd9;\n"
 840        << "    localparam S_RESET_NFA = 4'd10;\n"
 841        << "    localparam S_QUERY_TX  = 4'd11;\n\n"
 842        << "    reg [3:0] state = S_RESET_NFA;\n\n"
 843        << "    reg [" << busHigh << ":0] snap_match = " << numNFAs << "'b0;\n"
 844        << "    reg [31:0]          snap_bytes = 32'd0;\n"
 845        << "    integer k;\n\n"
 846        << "    always @(posedge clk) begin\n"
 847        << "        tx_send    <= 1'b0;\n"
 848        << "        fifo_rd_en <= 1'b0;\n"
 849        << "        nfa_en     <= 1'b0;\n"
 850        << "        nfa_start  <= 1'b0;\n\n"
 851        << "        if (rst_btn) begin\n"
 852        << "            state      <= S_RESET_NFA;\n"
 853        << "            match_leds <= " << numNFAs << "'b0;\n"
 854        << "            byte_count <= 32'd0;\n"
 855        << "            for (k = 0; k < 16; k = k + 1)\n"
 856        << "                match_count[k] <= 16'd0;\n"
 857        << "        end else begin\n"
 858        << "            case (state)\n\n"
 859        << "                S_IDLE: begin\n"
 860        << "                    nfa_end_of_str <= 1'b0;\n"
 861        << "                    if (!fifo_empty) state <= S_FETCH;\n"
 862        << "                end\n\n"
 863        << "                S_FETCH: begin\n"
 864        << "                    fifo_rd_en <= 1'b1;\n"
 865        << "                    state      <= S_DECODE;\n"
 866        << "                end\n\n"
 867        << "                S_DECODE: begin\n"
 868        << "                    if (fifo_rd_data == 8'h0A || fifo_rd_data == 8'h0D) begin\n"
 869        << "                        nfa_end_of_str <= 1'b1;\n"
 870        << "                        state          <= S_EOL_END;\n"
 871        << "                    end else if (fifo_rd_data == 8'h3F) begin  // '?'\n"
 872        << "                        state <= S_QUERY_TX;\n"
 873        << "                    end else begin\n"
 874        << "                        nfa_char_in <= fifo_rd_data;\n"
 875        << "                        state       <= S_CHAR_LOAD;\n"
 876        << "                    end\n"
 877        << "                end\n\n"
 878        << "                S_CHAR_LOAD: state <= S_CHAR_STEP;\n\n"
 879        << "                S_CHAR_STEP: begin\n"
 880        << "                    nfa_en     <= 1'b1;\n"
 881        << "                    byte_count <= byte_count + 1;\n"
 882        << "                    state      <= S_IDLE;\n"
 883        << "                end\n\n"
 884        << "                S_EOL_END: begin\n"
 885        << "                    nfa_en <= 1'b1;\n"
 886        << "                    state  <= S_EOL_MATCH;\n"
 887        << "                end\n\n"
 888        << "                S_EOL_MATCH: begin\n"
 889        << "                    nfa_en         <= 1'b1;\n"
 890        << "                    nfa_end_of_str <= 1'b0;\n"
 891        << "                    state          <= S_EOL_LATCH;\n"
 892        << "                end\n\n"
 893        << "                S_EOL_LATCH: begin\n"
 894        << "                    snap_match <= match_bus;\n"
 895        << "                    snap_bytes <= byte_count;\n"
 896        << "                    match_leds <= match_bus;\n"
 897        << "                    for (k = 0; k < " << numNFAs << "; k = k + 1)\n"
 898        << "                        if (match_bus[k]) match_count[k] <= match_count[k] + 16'd1;\n"
 899        << "                    state <= S_TX_ARM;\n"
 900        << "                end\n\n"
 901        << "                S_TX_ARM: begin\n"
 902        << "                    build_response(snap_match, snap_bytes);\n"
 903        << "                    tx_send <= 1'b1;\n"
 904        << "                    state   <= S_TX_WAIT;\n"
 905        << "                end\n\n"
 906        << "                S_TX_WAIT:\n"
 907        << "                    if (tx_state == TX_IDLE && !tx_send) state <= S_RESET_NFA;\n\n"
 908        << "                S_RESET_NFA: begin\n"
 909        << "                    nfa_start <= 1'b1;\n"
 910        << "                    nfa_en    <= 1'b1;\n"
 911        << "                    state     <= S_IDLE;\n"
 912        << "                end\n\n"
 913        << "                S_QUERY_TX: begin\n"
 914        << "                    build_response(" << numNFAs << "'b0, byte_count);\n"
 915        << "                    tx_send <= 1'b1;\n"
 916        << "                    state   <= S_TX_WAIT;\n"
 917        << "                end\n\n"
 918        << "                default: state <= S_IDLE;\n"
 919        << "            endcase\n"
 920        << "        end\n"
 921        << "    end\n\n"
 922        << "endmodule\n";
 923}
 924
 925// =============================================================================
 926// emitConstraints -  constraints.xdc
 927// =============================================================================
 928void Emitter::emitConstraints(const std::vector<std::unique_ptr<NFA>> &nfas, const std::filesystem::path &outputDir)
 929{
 930    auto filePath = outputDir / "constraints.xdc";
 931    std::ofstream out(filePath);
 932    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 933
 934    out << "## Clock signal (Nexys A7 100MHz)\n"
 935        << "set_property PACKAGE_PIN E3 [get_ports clk]\n"
 936        << "set_property IOSTANDARD LVCMOS33 [get_ports clk]\n"
 937        << "create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk]\n\n";
 938
 939    out << "## USB-RS232 Interface (Nexys A7)\n"
 940        << "set_property PACKAGE_PIN C4 [get_ports uart_rx_pin]\n"
 941        << "set_property IOSTANDARD LVCMOS33 [get_ports uart_rx_pin]\n\n"
 942        << "set_property PACKAGE_PIN D4 [get_ports uart_tx_pin]\n"
 943        << "set_property IOSTANDARD LVCMOS33 [get_ports uart_tx_pin]\n\n";
 944
 945    out << "## Buttons (Nexys A7 Center Button - BTNC)\n"
 946        << "set_property PACKAGE_PIN N17 [get_ports rst_btn]\n"
 947        << "set_property IOSTANDARD LVCMOS33 [get_ports rst_btn]\n\n";
 948
 949    out << "## LEDs (Nexys A7 LED0 to LED15)\n";
 950    std::vector<std::string> led_pins = {
 951        "H17", "K15", "J13", "N14", "R18", "V17", "U17", "U16",
 952        "V16", "T15", "U14", "T16", "V15", "V14", "V12", "V11"};
 953    for (size_t i = 0; i < nfas.size() && i < led_pins.size(); ++i)
 954    {
 955        out << "set_property PACKAGE_PIN " << led_pins[i] << " [get_ports {match_leds[" << i << "]}]\n"
 956            << "set_property IOSTANDARD LVCMOS33 [get_ports {match_leds[" << i << "]}]\n";
 957    }
 958}
 959
 960void Emitter::emitPIIUART(const std::filesystem::path &outputDir)
 961{
 962    auto filePath = outputDir / "uart_rx.v";
 963    std::ofstream out(filePath);
 964    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
 965
 966    out << "`timescale 1ns / 1ps\n\n";
 967    out << R"(
 968module uart_rx #(
 969    parameter CLKS_PER_BIT = 868 // 100 MHz / 115200 Baud
 970)(
 971    input  wire       clk,
 972    input  wire       rx,
 973    output reg  [7:0] rx_data,
 974    output reg        rx_ready
 975);
 976    localparam IDLE = 2'b00, START_BIT = 2'b01, DATA_BITS = 2'b10, STOP_BIT = 2'b11;
 977    reg [1:0] state = IDLE;
 978    reg [9:0] clk_count = 0;
 979    reg [2:0] bit_idx = 0;
 980
 981    always @(posedge clk) begin
 982        case (state)
 983            IDLE: begin
 984                rx_ready <= 1'b0;
 985                clk_count <= 0;
 986                bit_idx <= 0;
 987                if (rx == 1'b0) state <= START_BIT;
 988            end
 989            START_BIT: begin
 990                if (clk_count == (CLKS_PER_BIT-1)/2) begin
 991                    if (rx == 1'b0) begin
 992                        clk_count <= 0;
 993                        state <= DATA_BITS;
 994                    end else state <= IDLE;
 995                end else clk_count <= clk_count + 1;
 996            end
 997            DATA_BITS: begin
 998                if (clk_count < CLKS_PER_BIT-1) begin
 999                    clk_count <= clk_count + 1;
1000                end else begin
1001                    clk_count <= 0;
1002                    rx_data[bit_idx] <= rx;
1003                    if (bit_idx < 7) bit_idx <= bit_idx + 1;
1004                    else state <= STOP_BIT;
1005                end
1006            end
1007            STOP_BIT: begin
1008                if (clk_count < CLKS_PER_BIT-1) begin
1009                    clk_count <= clk_count + 1;
1010                end else begin
1011                    // FIX: Check for framing error
1012                    if (rx == 1'b1) rx_ready <= 1'b1; 
1013                    state <= IDLE;
1014                end
1015            end
1016        endcase
1017    end
1018endmodule
1019)";
1020}
1021
1022void Emitter::emitPIIUARTTx(const std::filesystem::path &outputDir)
1023{
1024    auto filePath = outputDir / "uart_tx.v";
1025    std::ofstream out(filePath);
1026    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
1027
1028    out << "`timescale 1ns / 1ps\n\n";
1029    out << R"(
1030module uart_tx #(
1031    parameter CLKS_PER_BIT = 868 // 100 MHz / 115200 Baud
1032)(
1033    input  wire       clk,
1034    input  wire       tx_start,
1035    input  wire [7:0] tx_data,
1036    output reg        tx,
1037    output reg        tx_busy
1038);
1039    localparam IDLE = 2'b00, START_BIT = 2'b01, DATA_BITS = 2'b10, STOP_BIT = 2'b11;
1040    reg [1:0] state = IDLE;
1041    reg [9:0] clk_count = 0;
1042    reg [2:0] bit_idx = 0;
1043    reg [7:0] tx_data_latch = 0;
1044
1045    initial begin
1046        tx = 1'b1;
1047        tx_busy = 1'b0;
1048    end
1049
1050    always @(posedge clk) begin
1051        case (state)
1052            IDLE: begin
1053                tx <= 1'b1;
1054                tx_busy <= 1'b0;
1055                if (tx_start) begin
1056                    tx_data_latch <= tx_data;
1057                    tx_busy <= 1'b1;
1058                    state <= START_BIT;
1059                    clk_count <= 0;
1060                end
1061            end
1062            START_BIT: begin
1063                tx <= 1'b0;
1064                if (clk_count < CLKS_PER_BIT-1) begin
1065                    clk_count <= clk_count + 1;
1066                end else begin
1067                    clk_count <= 0;
1068                    bit_idx <= 0;
1069                    state <= DATA_BITS;
1070                end
1071            end
1072            DATA_BITS: begin
1073                tx <= tx_data_latch[bit_idx];
1074                if (clk_count < CLKS_PER_BIT-1) begin
1075                    clk_count <= clk_count + 1;
1076                end else begin
1077                    clk_count <= 0;
1078                    if (bit_idx < 7) bit_idx <= bit_idx + 1;
1079                    else state <= STOP_BIT;
1080                end
1081            end
1082            STOP_BIT: begin
1083                tx <= 1'b1;
1084                if (clk_count < CLKS_PER_BIT-1) begin
1085                    clk_count <= clk_count + 1;
1086                end else begin
1087                    state <= IDLE;
1088                end
1089            end
1090        endcase
1091    end
1092endmodule
1093)";
1094}
1095
1096void Emitter::emitPIITopFPGA(const std::vector<std::unique_ptr<NFA>> &nfas, const std::filesystem::path &outputDir)
1097{
1098    auto filePath = outputDir / "top_fpga.v";
1099    std::ofstream out(filePath);
1100    out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
1101
1102    size_t numNFAs = nfas.size();
1103
1104    out << "`timescale 1ns / 1ps\n\n";
1105    out << "module top_fpga (\n"
1106        << "    input  wire clk,        // 100 MHz system clock\n"
1107        << "    input  wire rst_btn,    // Physical reset button\n"
1108        << "    input  wire uart_rx_pin,// USB UART RX pin\n"
1109        << "    output wire uart_tx_pin // USB UART TX pin\n"
1110        << ");\n\n";
1111
1112    out << R"(
1113    wire [7:0] rx_data;
1114    wire       rx_ready;
1115    
1116    uart_rx uart_inst (
1117        .clk(clk),
1118        .rx(uart_rx_pin),
1119        .rx_data(rx_data),
1120        .rx_ready(rx_ready)
1121    );
1122
1123    reg        nfa_start = 1'b1;
1124    reg  [7:0] nfa_char_in = 8'h00;
1125    reg        nfa_en = 1'b0;
1126)";
1127
1128    out << "    wire [" << (numNFAs > 0 ? numNFAs - 1 : 0) << ":0] match_bus;\n"
1129        << "    wire [" << (numNFAs > 0 ? numNFAs - 1 : 0) << ":0] active_bus;\n\n";
1130
1131    out << "    top regex_engine (\n"
1132        << "        .clk(clk),\n"
1133        << "        .en(nfa_en),\n"
1134        << "        .rst(rst_btn),\n"
1135        << "        .start(nfa_start),\n"
1136        << "        .char_in(nfa_char_in),\n"
1137        << "        .match_bus(match_bus),\n"
1138        << "        .active_bus(active_bus)\n"
1139        << "    );\n\n";
1140
1141    out << R"(
1142    // UART TX Signals
1143    reg        tx_start = 1'b0;
1144    reg  [7:0] tx_data = 8'h00;
1145    wire       tx_busy;
1146
1147    uart_tx uart_tx_inst (
1148        .clk(clk),
1149        .tx_start(tx_start),
1150        .tx_data(tx_data),
1151        .tx(uart_tx_pin),
1152        .tx_busy(tx_busy)
1153    );
1154
1155    // 128-Byte Latency Buffering with Precise Alignment
1156    localparam DELAY_LEN = 128;
1157    (* ram_style = "distributed" *) reg [7:0] delay_bram [0:DELAY_LEN-1];
1158    reg [6:0] delay_ptr = 0;
1159    reg [DELAY_LEN-1:0] commit_history = 0;
1160
1161    // Per-NFA Active Histories to isolate redaction contexts
1162)";
1163    for (size_t i = 0; i < numNFAs; ++i)
1164    {
1165        out << "    reg [DELAY_LEN-1:0] active_history_" << i << " = 0;\n";
1166    }
1167
1168    out << R"(
1169    // Power-On-Reset Initialization
1170    reg [7:0] por_count = 0;
1171    wire por_rst = (por_count < 8'hFF);
1172
1173    reg rx_ready_prev = 0;
1174    reg [1:0] step = 0;
1175    reg [7:0] rx_latch = 0;
1176
1177    always @(posedge clk) begin
1178        if (rst_btn || por_rst) begin
1179            if (por_count < 8'h80) begin
1180                delay_bram[por_count[6:0]] <= 8'h20; // Initialize BRAM sequentially
1181            end
1182            if (por_count < 8'hFF) por_count <= por_count + 1;
1183            
1184            nfa_en <= 0;
1185            nfa_start <= 1;
1186            tx_start <= 0;
1187            rx_ready_prev <= 0;
1188            delay_ptr <= 0;
1189            commit_history <= 0;
1190            step <= 0;
1191)";
1192    for (size_t i = 0; i < numNFAs; ++i)
1193    {
1194        out << "            active_history_" << i << " <= 0;\n";
1195    }
1196    out << R"(
1197        end else begin
1198            nfa_start <= 0;
1199            nfa_en <= 0;
1200            tx_start <= 0;
1201            rx_ready_prev <= rx_ready;
1202
1203            // Character Processing Sequence
1204            if (rx_ready && !rx_ready_prev) begin
1205                rx_latch <= rx_data;
1206                step <= 1;
1207            end
1208
1209            case (step)
1210                1: begin
1211                    // Step 1: Feed NFA Engine
1212                    nfa_char_in <= rx_latch;
1213                    nfa_en <= 1;
1214                    step <= 2;
1215                end
1216                2: begin
1217                    // Step 2: NFA Results and Redaction Output
1218                    // 1. Read delayed char and check redaction bit (Sampling BEFORE shift)
1219                    tx_data <= (commit_history[DELAY_LEN-2]) ? 8'h58 : delay_bram[delay_ptr];
1220                    tx_start <= 1;
1221
1222                    // 2. Overwrite slot with new character
1223                    delay_bram[delay_ptr] <= rx_latch;
1224                    delay_ptr <= delay_ptr + 1;
1225
1226                    // 3. Update Histories (Isolating contexts)
1227)";
1228    for (size_t i = 0; i < numNFAs; ++i)
1229    {
1230        out << "                    active_history_" << i << " <= active_bus[" << i << "] ? {active_history_" << i << "[DELAY_LEN-2:0], 1'b1} : 128'd0;\n";
1231    }
1232
1233    out << "                    commit_history <= {commit_history[DELAY_LEN-2:0], 1'b0}";
1234    for (size_t i = 0; i < numNFAs; ++i)
1235    {
1236        out << "\n                                      | (match_bus[" << i << "] ? {active_history_" << i << "[DELAY_LEN-2:0], 1'b1} : 128'd0)";
1237    }
1238    out << ";\n";
1239
1240    out << R"(
1241                    step <= 0;
1242                end
1243            endcase
1244        end
1245    end
1246endmodule
1247)";
1248}