One-more-re-nightmare – A fast regex compiler in Common Lisp
applied-langua.ge
applied-langua.ge
More specifically, you can compile syntax (of your choosing) into performant, low-level Lisp code, which the Lisp compiler can then compile into efficient machine code. To do this, you need no extra tools, no intermediate files, no compiler hooks, no virtual machines, no byte codes, and no non-standard or non-portable features. It has been built in to Lisp since at least the 1980s.
Making miniature "JIT" compilers is also a technique a quantum simulator in Lisp [0a, 0b] uses to optimize quantum gate operations. It analyzes the quantum gate matrix and, on the fly, compiles it into optimized numerical Lisp code that is equivalent to that matrix acting on vectors of tensor product spaces (i.e., super huge arrays of complex double floats). Using another language would require me to do arduous things like write assembly code or learn a library like LibJIT [1] which may not even interface well with my host language.
Other simulators out there written in C use huge hand-unrolled loops with comments like
// THIS CODE WAS GENERATED, DO NOT TOUCH
and/or in-line assembly code, and still can't optimize specific matrix shapes and structures, or do algebraic simplifications to eliminate work altogether.The regex library FTA is a great, and clean, example of a long standing practice of compiling regexen, except it doesn't use any fancy VMs or any fancy JITs, just "when you see this regex, automatically turn it into this Common Lisp code, and let the Lisp compiler handle the rest."
[0a] https://github.com/quil-lang/qvm
[0b] COMPILE-OPERATOR: https://github.com/quil-lang/qvm/blob/master/src/compile-gat...
This was a problem for early C++ compilers like Cfront that translated C++ into C: the error messages from the C compiler referenced the intermediate C, not the original C++.
https://stackoverflow.com/questions/2216540/whats-the-meanin...
there are nice performance comparisons to glibc, rust regex and cl-ppcre, but re2 was missing from that list.
I got worried at some point that some of the purported advantages of derivative-based regexes might be drawbacks. One such advantage/drawback is that it adds the complement operation into the language of Regex. But that gives you a way of deciding whether two regexes are equivalent, which is supposed to be PSPACE-COMPLETE, which ought to severely hamper finding an efficient implementation.
If you don't use submatching, you can just compute the minimal DFAs and compare them to test for equivalence, and you don't even need negation. Thus, introducing negation can't do any harm there.
But as I generate Mealy machines for submatching, which can't be minimised, we need another approach. Knowing that A - B = A ∧ ¬B, one can produce a DFA for (A - B) ∨ (B - A), and if it has no accepting states, then A and B must be equivalent, as nothing can match one but not both. But DFA construction is O(2ⁿ), even if one takes the RE → NFA → DFA route, so this is nothing new complexity-wise.
Such a decision procedure is never used in a derivative-based regex implementation, anyway. The equivalence of regular expressions only needs to be approximated, and the approximation consists of some structural rules (see Definition 4.1 of [1]). Relatively few rules are needed to guarantee that a _finite_ machine can be generated, and only a dozen rules are required to get very close to a minimal DFA (table 1, page 14 of [1]). So having the possibility of writing a precise regex equivalence function is completely irrelevant to producing an efficient regex engine.
A decision procedure is handy, but you can write one for submatch-less regexes without negation, the complexity of such a procedure is typical for DFA construction, and the problem of deciding if two regular expressions are equivalent is irrelevant.
[1] Regular-expression derivatives reexamined https://www.ccs.neu.edu/home/turon/re-deriv.pdf
Equivalence is required for compiling. The reason is that regular expressions have features like the Kleene closure which express iteration, and these give rise to endless derivatives: regular expressions whose derivative you can keep taking ad nauseum. This is fine for interpreting, because the algorithm terminates when you have consumed the input or hit an impasse. The compiler, however, has to close the loops and turn them into cycles in the state machine graph. To do that it has to check each derivative: "have I derived an expression which has occurred before?" If so, then just hook to the previous state.
If you make the check literal, just comparing the regex syntax for identical structure, the graph will sometimes be larger than necessary, because it will contain redundant nodes that should be equivalent states. The smarter you make the equivalence function, the more compact the graphs will be.
Languages are rarely "slow" or "fast" by themselves. Programs written in those languages can be slow or fast.
For sure they are, language features will for sure limit the optimizations a compiler/interpreter can do and for sure a language with many levels of abstractions will have to pay the price in performance somewhere.
Every-time someone improves the benchmark performance of a slow language that person is not a regular developer, but someone that has a deeper understanding on what happens under the hood, knows how to use profiling tools, maybe knows assembly and most of the time they have to use tricks or advanced patterns.
Btw, optimizing some slow language in a hot path with clever tricks or some advanced patterns is fine, it happens all the time, you can;t just switch the project language with 1 click.
For example:
Common Lisp has a full numerical tower, meaning it will automatically convert fixed size numbers into bignums (and more cool stuff). This means that an addition has to branch if it can't prove that a result won't overflow from fixnum to bignum. Yes, C also needs to use bignums, but they're on an opt-in basis. In CL they're opt-out.
There are more examples of this, often related to the dynamism of your language. The more introspection a language is capable of, the less a compiler designer can do to cheat and optimize your program.
https://european-lisp-symposium.org/static/proceedings/2021....
(the last paper, by Robert Strandh.)
Common Lisp is interesting in that it is high-level while lots of implementations (like sbcl) also retaining the ability to drop into lower levels (even assembly) if lots of performance is needed.