Sequential Circuits from Regular Expressions Revisited
Dogan Ulus
cs.FL


Data-Driven Inversion-Based Control: 
closed-loop stability analysis
for MIMO systems
    Carlo Novara and Simone FormentinC. Novara is with Politecnico di Torino (Italy), S. Formentin is with
Politecnico di Milano (Italy). Email: ,
.
    December 30, 2023
====================================================================================================================================================




We revisit the long-neglected problem of sequential circuit constructions from regular expressions. 

The class of languages that are recognized by sequential circuits is equivalent to the class of regular languages. 

This fact is shown in <cit.> together with an inductive construction technique from regular expressions.

In this note, we present an alternative algorithm, called the trigger-set approach, obtained by reversing well-known follow-set approach to construct automata.

We use our algorithm to obtain a regular expression matcher based on sequential circuits. 

Finally, we report our performance results in comparison with existing automata-based matchers.




§ INTRODUCTION


A sequential circuit receives an input value at each step, updates its (internal) state, and yields an output value.

The current state of a circuit represents the history of input values received during its operation.

Circuits store their current state inside memory elements (e.g. flip-flops or registers) and update it with respect to newly received input value.

Then, the circuit yields an output value at each step, which is determined by the current state and input in general.

Clearly this is a simple but powerful model of computation and apparently impressed many people from electrical engineering and computer science over years.

While engineers were building modern computers upon sequential circuits, computer scientists studied abstract models of them.

Today we call these abstract models finite automata but their original relation to sequential circuits is largely forgotten.

In this note, we study sequential circuits with a goal to obtain regular expression matchers, which is an application historically that employs automata and related techniques <cit.>.

Interestingly enough, the use of automata were also forgotten for pattern matching purposes in the sake of search-based (backtracking) methods until it is revived in a blog post[<https://swtch.com/ rsc/regexp/regexp1.html>] by Russ Cox. 

Eventually this effort has provided us industrial-grade pattern matchers that use automata (again) such as Google's re2 engine[<https://github.com/google/re2>].

Besides recent programming languages such as go and rust implement automata inside their regular expression engines unlike perl and python that implement backtracking.

It is worth to note that existing backtracking and automata based matchers are mostly comparable in performance.

However, there are (pathological) cases that backtracking fails miserably and requires exponential time for matching whereas automata have a formal guarantee for linear execution time.

So what can we gain from sequential circuits in this already-quite-developed world of pattern matching?

First, we do not want a worst-case exponential time algorithm, which can be more critical as we will eventually go beyond text processing as in <cit.>.

Therefore, we eliminate backtracking matchers from our discussion and focus on the other class.

Automata based matchers usually implement two types[Some can have more. For example, re2 implements an intermediate form called one-pass nfa for patterns having limited non-determinism.] of automata, namely deterministic (dfas) and non-deterministic (nfas).

Roughly speaking, for a given expression with a size m, dfas tend to be large (exponentially bounded by m) and fast (constant to m) whereas nfas are small in size (linearly bounded by m) and slow (quadratic to m).

Note that actual performances considerably depend on the expression as well as the input string to be matched.

Sequential circuits would reside at the non-deterministic side of this spectrum but they seem to be better to handle non-determinism than nfas.

It is not wrong to consider sequential circuits to be a yet another way to implement non-deterministic automata —remember that automata are abstract models of sequential circuits— but we argue that this view is missing some essence.

There is one important operational contrast between automata and sequential circuits regarding their update mechanisms.

For automata, update mechanism operates by pushing the information forward. 

The information about being in a state is propagated to the next states with respect to the current input so we compute reachable successors. 

On the contrary, update mechanism for sequential circuits operates by pulling the information from back.

This time we independently compute the reachability of each state from predecessors with respect to the current input.

By analogy, we can say that automata run by a rear-wheel-drive whereas sequential circuits a front-wheel one.

Both computations are going to exactly the same direction but the difference in update mechanisms creates some interesting trade-offs in practice.

Maybe a 4-wheel drive is what we want.

In the following, we first review sequential circuit briefly and then propose an algorithm to directly construct sequential circuits from (classical) regular expressions. 

The last section reports the implementation, test cases, and some comparison with re2's dfa and nfa implementations.



§ SEQUENTIAL CIRCUITS


A sequential circuit reads a sequence X_1X_2… of k-dimensional Boolean vectors such that X_n∈𝔹^k for n = 1, 2, ….

Clearly any finite alphabet Σ can be encoded as a set of k-dimensional Boolean vectors for a suitable k with typical examples of ascii and Unicode.

Therefore, in this note, we interchangeably use Boolean vectors and letters as inputs to circuits.

Then, we characterize a sequential circuit by three elements as follows.


    [                                                    V                                                    :                 m+1 dimensional Boolean state vector;                                            F_i(V, X)                                                    : Next-state functions for each position i = 0, 1,…, m;                                              Y(V, X)                                                    :                   The output function of the circuit ]


such that X∈𝔹^k is the current input value.

The functions F_i and Y are Boolean functions from 𝔹^m+1×𝔹^k.

We denote by V_n the valuation (content) of the vector V at the time step n and we call V_0 the initial valuation of the circuit.

The circuit operates by reading the current input value X_n, updating the state vector V such that


    V_n(i) = F_i(V_n-1, X_n)   for  i = 0, …, m


and yielding a Boolean value Y_n such that


    Y_n = Y(V_n-1, X_n)


at each time step n. 

The output of a sequential circuit is completely determined by the initial valuation V_0 and the sequence of input values read so far.

This is called sequential model of computation, which relates input sequences to output sequences as illustrated in Figure <ref>.

It is customary to say that any sequence of input values that make the circuit to yield 1 is accepted by the circuit for a fixed initial valuation.

For example, input sequences a_1, a_1a_2a_3, and a_1a_2a_3a_4a_5 are accepted by the circuit in Figure <ref> whereas a_1a_2 and a_1a_2a_3a_4 are rejected.



The set of all input sequences accepted by a sequential circuit is called the language of the circuit.

It is shown in <cit.> that all and only regular languages recognized by sequential circuits as an analogue of Kleene's theorem <cit.>.

These concepts are of course familiar to anyone who studied automata theory, which is established in classical papers <cit.> as an abstract treatment of sequential circuits.



However, reinstalled circuit approach here makes it clearly visible that checking acceptance or matching a word can be reduced to a computation of a set of Boolean functions repeatedly. 

Especially we emphasize that (more) expensive list processing operations in nfa simulations can be avoided if we had constructed circuits instead of automata.

Besides the efficiency can be easily increased by exploiting parallelism of the hardware if needed. 

On a fully parallel hardware[We say so because our circuit definition is still more mathematical than electrical.], circuits are probably the most efficient representations of regular languages since they would have a small (linear) size and operate very fast (one clock period per one letter of the input).

This fact is rather well-known in hardware communities <cit.> but methods involve a translation into dfa or nfa, which brings additional work, if not complexity (e.g. ϵ-transitions).



§ SEQUENTIAL CIRCUITS FROM REGULAR EXPRESSIONS


In this section, we present an algorithm to construct sequential circuits directly from regular expressions, which are syntactic representations of regular languages. 

We give the syntax of regular expressions in this note by the following grammar.


    E :=  a | E_1∪ E_2 | E_1· E_2 | E^* | E^+ | E^?


where a is a letter of an alphabet Σ and E is a regular expression. The operators · and ∪ denote concatenation and union as usual. Other operators *, +, and ? are repetition operators and sometimes called zero-or-more, one-or-more, and zero-or-one repetition, respectively.

Our algorithm falls into the family of position-based construction algorithms that associate each letter (or sub-expression) of the expression with a unique position value. 

Previous works <cit.> in this family[Inductive constructions such as Thompson's <cit.> are also in this family as positions are implicitly assumed. The other family is called derivative-based constructions stemmed from Brzozowski's derivatives <cit.>. Berry and Sethi show that these two happy families are alike <cit.>.] are interested in computing succeeding positions (follow sets) for each position in the expression.

The rough idea here is that the acceptance procedure for an input word would resemble the act of going from position to position on the expression.

Consequently, these positions become states of an nfa and follow sets make the transition function.

On the other hand, we are interested in computing trigger conditions (or preceding positions) for each position to obtain a sequential circuit.

To this end, we closely follow the work of Berry and Sethi <cit.> but reverse the follow set approach. 

The proposed treatment is mostly symmetric and yields a sequential circuit instead of an nfa as the outcome.

This result can be taken as yet another demonstration of the close relation between automata and sequential circuits.

We start by marking mark all letters of a regular expression E to make them distinct. 

The marked version of a regular expression E is obtained by associating letters in E with a number called position. 

Positions are denoted by subscripts over letters. 

In the strict sense, marked letters a_i and a_j are distinct if i ≠ j.

We designate the position 0 as the initial position of the expression and then enumerate letters left to right. 

The leftmost letter in E is associated with the position 1 and the rest goes on. 

For example, an expression (a· b ∪ b)^*· b · a becomes (a_1· b_2 ∪ b_3)^*· b_4 · a_5 after marking process.

Finally, we say that the size #(E) of an expression E is the number of letters in the expression.

In the following, we present three functions, , , and  that have been used to construct a sequential circuit from a marked regular expression.



First, the function  checks whether the language of an expression E contains the empty word (thus whether E is skippable). 

This function can be computed inductively for regular expressions by the following rules.


    [           (a_i)               =               0;      (E_1∪ E_2)               =  (E_1) or (E_2);      (E_1· E_2)               = (E_1) and (E_2);           (φ^*)               =               1;           (φ^+)               =             (φ);           (φ^?)               =               1;                 ]


Second, we define the  function that computes the outputting (or last) positions of each node. 

The computation of the  function is inductively given by the following rules.


    [                                     (a_i)                                         =                                       {i};                                (E_1∪ E_2)                                         =                               (E_1)∪(E_2);                                (E_1· E_2)                                         = (E_1)∪(E_2)   if (E_2)
    (E_2)   otherwise.;                                     (E^*)                                         =                                       (E);                                     (E^+)                                         =                                       (E);                                     (E^?)                                         =                                       (E);                                           ]




Continuing our running example, we compute  and  functions and illustrate over the syntax tree in Figure <ref>.

We annotate each node of the syntax tree with corresponding value computed by  and  functions.

Third, recall that we have reserved the position 0 as the starting position.

We use the position 0 as the trigger for the expression and it allows us to control where to start a match on the input sequence.

We will explain this point later with an example.  

The function (E, {0}) yields a set of triples (i, a, H) for a given regular expression E where i is a position, a is the corresponding letter, and H is a set of positions that trigger the position i together with a. 

In other words, one should be in a position j∈ H and read the letter a to reach the position i in the next step. 

Then, we inductively define the function  as follows.


    [                                                                 (a_i, H)                                                                        =                                                              {(i, a, H)};                                                            (E_1∪ E_2, H)                                                                        =                                                        (E_1, H)∪(E_2, H);                                                            (E_1· E_2, H)                                                                        = (E_1, H)∪(E_2, (E_1) ∪ H)    if (E_1)
    (E_1, H)∪(E_2, (E_1))   otherwise.;                                                                 (E^*, H)                                                                        =                                                             (E, (E) ∪ H);                                                                 (E^+, H)                                                                        =                                                             (E, (E) ∪ H);                                                                 (E^?, H)                                                                        =                                                                   (E, H);                                                                          ]


An example computation of trigger sets is give over our running example in Figure <ref>.

By computing (E, {0}), we have almost finished our sequential circuit construction but we must also define initializations and the output function. 

The output function is determined by (E) function at the top level. 

For the starting behavior, we have two typical options: (1) Start matching only from the beginning of the input sequence. This is also known as the acceptance, full match, etc. (2) Start anywhere on the input sequence. This is also known as the suffix acceptance, partial match, etc., which is indeed equivalent to the acceptance problem of Σ^*· E.

These starting behaviors is determined by the next-state function F_0 of the initial position.

Finally, we present the full construction in Algorithm <ref> and the circuit generated for the expression ((a· b) ∪ b )^*· b · a in Figure <ref>.













§ IMPLEMENTATION


The prototype implementation[<https://github.com/doganulus/reelay>] of the trigger set algorithm performs two main tasks. 

The first task is in computing #, , , and  functions for the regular expression given.

Corresponding visitor implementations annotate the syntax tree according to definitions in the previous section and return the size of the expression, the output set, and trigger sets for each position.

Then, using this information, we construct a sequential circuit from the expression, that is to say, generate a c++ code to be compiled into a program that matches the expression over the input word.

Generated code is very simple and currently requires the name of a file that contains the input word as its only argument.

For example, in Listing 1, we show the code generated for the sequential circuit given in Figure <ref>.





We compare our sequential circuit based implementation against automata based regular expression matcher re2's dfa and nfa implementations.

We generate a similar code that uses re2 for testing purposes. 

To this end, we simply call re2's corresponding matching function () instead of our sequential circuit implementation.

Finally note that re2 constructs a dfa by default. 

In order to force re2 to construct nfa, we reduce maximum allowed cache for dfa simulation so that it falls back to nfa.

These codes that use re2 can be found in the appendix.

We compile the generated code using standard  compiler with level two optimizations ().

In our tests, we perform suffix matching for a test pattern over very long (67M characters = 67mb) random sequences of textual characters over an alphabet Σ.

All tests run on a 3.3GHz machine.

Since the execution time is linear to the input size, we only report the throughput of matching (in mb per second) obtained by dividing input file size by the minimum of 10 actual execution times.

Then we present our experimental results in Tables 1-5 and discuss them in the following section. 



§ DISCUSSION


In this note, we presented an algorithm to construct a sequential circuit from regular expressions.

We implemented our algorithm straightforwardly and tested sequential circuit approach against a well-engineered regular expression engine that implements automata.

In our tests, sequential circuits clearly outperforms standard nfas, which shares similar memory requirements.

There is one case (shown in Table 2) that nfa seems faster but this is probably due to that re2's engine falls back to one-pass nfa rather than standard nfa since the pattern is unambiguous.

It is no surprise that deterministic automata are faster than sequential circuits when the pattern is more deterministic and not too large.

However, there are patterns when the number of deterministic states grows exponentially in the size of expression.

The pattern given in Table 5 is not randomly selected and similar to the example used in <cit.>, which proves the exponential bound on nfa-dfa conversion is indeed tight.

In short, this is very bad news for dfa.

The dfa implementation in re2 performs a lazy determinization procedure, which computes and stores new deterministic states only when it is required.

The number of deterministic states kept in the cache is capped by a fixed value (the default must be around 10K states) and the implementation falls back to nfa if this limit is violated often.

This perhaps explains the dramatic performance decrease observed for dfa between n=14 and n=15 in Table 5. 

On the contrary, sequential circuits perform better for such highly non-deterministic patterns.

Therefore, we believe that sequential circuits offer an alternative solution for the pattern matching problem and can replace non-deterministic automata to this end. 









§ GENERATED CODES


[frame=single, language=c++, breaklines=true, basicstyle=, caption=Example test code for RE2 (DFA)]
#include <iostream>
#include <fstream>
#include <re2/re2.h>

int main(int argc, char **argv) 

	std::ifstream ifs(argv[1]);
	std::string word((std::istreambuf_iterator<char>(ifs)),(std::istreambuf_iterator<char>()));

	    std::cout << RE2::PartialMatch(word, "(?:(?:(?:ab)|b)*)ba") << std::endl;

[frame=single, language=c++, breaklines=true, basicstyle=, caption=Example test code for RE2 (Forced NFA)]
#include <iostream>
#include <fstream>
#include <re2/re2.h>

int main(int argc, char **argv) 

    std::ifstream ifs(argv[1]);
    std::string word((std::istreambuf_iterator<char>(ifs)),(std::istreambuf_iterator<char>()));

    RE2::Options opt;
    opt.set_max_mem(2048);
    RE2 re("(?:(?:(?:ab)|b)*)ba", opt);

    std::cout << RE2::PartialMatch(word, re) << std::endl;