From my own experiments[1], I suspect your engine might still be prone to exponential blowup: you only get proper linear matching if you reassociate and sort the choice expressions and merge identical terms (and Brzozowski’s original paper[2] does point this out). This makes the implementation quite a bit more annoying.
ETA: Yup. Try (successfully) matching a long string of As against (A*)* and watch it die.
I saw this and I was immediately sure that I could make this blow up exponentially. But then I got too lazy to think about exactly how, and instead waited for kind strangers on the Internet to provide a ready solution. Thank you :)
>> I suspect your engine might still be prone to exponential blowup
> DFA construction has a risk of exponential growth in the number of states.
Right, I may have phrased that badly. I meant that the length of the derivative can grow multiplicatively for each consumed character, without bound, so even with memoization you won’t end up with a DFA. For example, in your simple implementation each A fed into (A*)* gives you a new (longer) RE, forever, even though a DFA for it only has to have a starting state and a failure one (actual implementations may end up with more).
Brzozowski proves that a RE has only a finite number of derivatives, thus making memoized differentiation equivalent to lazy DFA construction, but only if you respect associativity, commutativity and idempotence of choice ( | in modern syntax, + in his). I’m not actually sure you need all of those, in all circumstances, or if it’s enough to restrict the equivalences to e.g. the vicinity of a repetition operator, and he doesn’t discuss this, but I’ve made a couple of simpler attempts and could still make them blow up after some tinkering.
I think I got your point. It is valid. To construct the DFA and have a finite number of derivatives we have to keep track of the following equivalences:
r + r ~ r
r + s ~ s + r
(r + s) + t ~ r + (s + t)
In [1] authors state this and refer to the proof in the original paper. They even extend it to a set of extended rules to reduce the number of terms (states) even more.
Actually, the code for (lazy) DFA construction code is not even committed yet. The repo contains just sequential per-character application of the derivative to a regex. Which is obviously finite (though not efficient). Again, just to demonstrate the concept.
Yes, and even if you aren’t constructing a DFA, only being able to produce a finite number of derivatives from a given RE is still useful:
As there’s only a finite number of derivatives, their length is obviously bounded by a constant for a fixed starting RE (though that constant is still exponential in the length of that RE). This implies your non-DFA-based matcher can only take a bounded time computing the next derivative, so takes a time proportional to the length of a string to process that string (even if the constant of proportionality is exponential in the RE length).
(I’m not good at fitting all of my reasoning into a single comment today, am I?)
ETA: Yup. Try (successfully) matching a long string of As against (A*)* and watch it die.
[1] http://ix.io/4qan/ (matching only), http://ix.io/4qap/ (adds parsing and printing).
[2] https://doi.org/10.1145/321239.321249