ECMAScript regular expressions are getting better (2017)
mathiasbynens.be
mathiasbynens.be
https://www.regular-expressions.info/freespacing.html https://www.regular-expressions.info/named.html
They're available today in Chrome, unsure about other browsers.
In OCaml, most people use the re[1] library for regular expressions (example[2]). I wrote a library called tyre[3] for typed extraction that follows a similar API. Of course these APIs are much more verbose, but they are also very regular(hah!): Regular operators are normal functions of the language, typechecking and completion works, etc.
[1]: https://github.com/ocaml/ocaml-re
[2]: https://github.com/ocaml/ocaml-re/blob/master/benchmarks/ben...
The maintenance problem for me with regular expressions is that when I write them I have carefully studied a variety of inputs and then make something that matches. Typically I will also have tested it as I go, trying out various known strings. But then a year later I have forgotten all the cases, and I have to somehow decompress them from the regex.
If I save my experiments in well-named unit tests, though, it's much easier for me to figure out my original intent, and to see whether the new case I'm thinking about is covered.
https://reference.wolfram.com/language/guide/StringPatterns....
NUMBER = ///
^ 0b[01]+ | # binary
^ 0o[0-7]+ | # octal
^ 0x[\da-f]+ | # hex
^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal
///i
[0]: https://coffeescript.org/#regexes Regex::new(r"(?x)
(?P<y>\d{4}) # the year
-
(?P<m>\d{2}) # the month
-
(?P<d>\d{2}) # the day
")(I'm the author.)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
regex parse error:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1: (?x)
2: (?P<y>\d{4}) # the year
3: -
4: (?P<m>*\d{2} # the month
^
5: -
6: (?P<d>\d{2}) # the day
7:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: repetition operator missing expression
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Playground link: https://play.rust-lang.org/?version=stable&mode=debug&editio...I'm not sure so much innovation in syntax is going to help adoption, but the aim was to make the syntax less code-golf-y and more readable.
Pattern matching with a verbose syntax that is "easy to ready" has been done hundreds of times but it rarely translates into "easy to read" for anyone familiar with regular expressions.
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
could be specified in your program as something like: ((:named-register "year" (:greedy-repetition 4 4 :digit-class))
"-"
(:named-register "month" (:greedy-repetition 2 2 :digit-class))
"-"
(:named-register "day" (:greedy-repetition 2 2 :digit-class)))
(Whether this is easier to read depends on your relative familiarity with CL and PCRE. It's a lot easier to generate or manipulate with native CL functions, though, if you ever need to do that.)Most HLLs today wrap an existing C regular expression library, and I don't know any C regular expression library that provides a public interface to its parse tree, so it's unlikely that other languages will be able to do something similar without a lot of work.
Of course, regular-expressions-as-strings are still strings, so if you only need to write them, you can get most of the benefit by using your language's native string facilities: https://news.ycombinator.com/item?id=241373
I don't think I know of one either. But Go's regexp library provides access to the syntax[1], and so does rust/regex[2]. In the case of [2], it provides both an AST and a high level IR for regexes. It's not as convenience to build expressions as in your Lisp example, though, there's nothing stopping someone from building such a convenience. :-)
Character classes beyond \w, d and b always require a doc-lookup and I always misremember the look{ahead,behind} operators.
The issue is that its too small to remember fully, makes no effort to be self-documenting, the docs are always annoying (I can never get a ^F search to not return 20 items), and theres always an uncovered edge case somehow
For some examples see: http://blog.hostilefork.com/why-rebol-red-parse-cool/
for (let i=20; i<30; i++) { let str = "a".repeat(i); let regex = new RegExp("a?".repeat(i) + "a".repeat(i)); let t0 = performance.now(); str.match(regex); let t1 = performance.now(); console.log(i + " took " + Math.round(t1 - t0) + " milliseconds."); }
20 took 7 milliseconds. 21 took 14 milliseconds. 22 took 27 milliseconds. 23 took 52 milliseconds. 24 took 102 milliseconds. 25 took 224 milliseconds. 26 took 421 milliseconds. 27 took 814 milliseconds. 28 took 1604 milliseconds. 29 took 3470 milliseconds.
Tested today on latest Chrome on a MacBook Pro 2,6GHz Intel Core i7.
Here comes an idea on what could be done to improve performance for regexes that are actually regular, and to still keep support for non-regular advanced regexes that e.g. contain back-references:
Add a step immediately after parsing the regex, check if it is regular and can be handled by an engine like RE2, otherwise handle it using the normal regex engine.
An even more exotic idea would be to let multiple regex engines execute in parallell, and return the result for the one that finishes first.
/a{1,30}/
Can you give an example of a regex that can't be easily re-written to be fast with the backtracking engine?So you can't shoot your foot with expressions that seem fine but send your cpu off into la-la land when asked to match certain input data.
It would make sense to worry about it if it were a common mistake, but I've never seen a gratuitously exponential regexp in the wild.
And it really happens. StackOverflow had a ~30 minute outage a couple years ago because of it: http://stackstatus.net/post/147710624694/outage-postmortem-j...
One of the key points here is that it's not always obvious from looking at a regex whether it will exhibit catastrophic backtracking (at least, to an untrained eye). Even more insidious, catastrophic backtracking may only happen on certain inputs, which means that tests won't necessarily track it either. Therefore, desiring a performance guarantee here is certainly not "strange."
The bottom line is that the various implementation strategies for regex engines have trade offs. On Internet forums, it's popular to "proclaim" for one side or the other. The backtracking advocates like to believe that catastrophic cases never happen or are easily avoided and the linear time advocates like to believe that so long as matching doesn't take exponential time, then it will be fast enough. :-)
Besides, people make mistakes all the time. Why not prevent the possibility?
In general, "obvious" optimizations like this are almost always harder than they seem.
One possible path would be to precisely characterize regexes on which both FSMs and backtrackers agree precisely, and of only those, use the FSM based approach. But still, even then, you're maintaining two regex engines instead of one, and both need to be production grade and very fast. It is no easy task.
/^abcd(a(b|c)*d)*a(bc)*d$/
I used long strings of repeated "abcdabcd" as the test strings.It's possible I made a mistake somewhere, and I can put together an open-source repo with the test setup when I get the time. But I'm curious why you find the results shocking?
One thing I spotted is that you're asking the native engines to record capture groups, but your wasm implementation doesn't support those. For fairness you might switch to non-capturing groups: `(?:bc)` instead of `(bc)`. However this cannot explain the magnitude of the difference.
I dug into it some more, reducing it to this case:
/^(?:abc*d)*$/
what happens is that the backtracking engine has to be prepared to backtrack into every iteration of the outermost loop. This means that the backtracking stack grows as the length of the input; the engine spends most of its time allocating memory! These nested loops definitely make the NFA approach look good.Regardless it's a cool project, thanks for sharing!
I think I had to build from source.
This would be a bad idea. For one thing, it would be slower on non-pathological cases, since backtracking is faster in practice. But more importantly, devs who test on Chrome might unwittingly create regexes that would run exponentially slower in other browsers.
That's most certainly not universally true. See: https://rust-leipzig.github.io/regex/2017/03/28/comparison-o...
The real answer is that it's complicated. Backtrackers can certainly be faster in some cases, but not all. Moreover, as someone who has worked on regex engines for a while now, it's not clear to me how exactly to characterize performance differences (outside of pathological cases or cases that otherwise invoke catastrophic backtracking), so my guess is that it's probably mostly up to implementation quality and the optimizations that are implemented.
(Notable exception: Rust https://docs.rs/regex/)
- Rust's regex library was greatly inspired by RE2, which is a C++ library that also executes regexes in linear time.
- Go's regex library also runs in linear time, however, it is still missing some optimizations that can make it run more slowly on non-pathological regexes when compared to RE2 and Rust's regex crate.
- You don't need to use fancy features with a backtracking regex engine in order to shoot yourself in the foot. e.g.,
>>> import re
>>> re.search('(a*)*c', 'a' * 30)
- Even with linear time regex engines, you can get big slow downs. You'll never get exponential (in the size of the text) slow downs of course, but regexes like `[01]*1[01]{20}$`[1] can generate large finite state machines, which can be problematic in either memory or match speed, depending on the implementation.[1] - https://cyberzhg.github.io/toolbox/min_dfa?regex=KDB8MSkqMSg...
/((a)|(b)|(c)|(d))*/
If the string has length N, the loop iterates N times, and each iteration must clear capture groups proportional to the length of the regex. So in this case the time varies as the product of the input and regex length, not their sum independently.Right, that's what RE2 and rust/regex both do. The lazy DFA finds the match, then something else resolves the capture groups.
This strategy doesn't always make sense, and it can be better to just run the NFA right away if the input is small.
If you find this stuff interesting, you'll definitely want to check out Russ Cox's article series on regexes. In particular: https://swtch.com/~rsc/regexp/regexp3.html
For example, if you do a Thompson NFA simulation (or, more practically, a Pike VM), then the time complexity is going to be O(mn), where m ~ len(regex) and n ~ len(input), regardless of capturing groups.
As another example, if you compile the regex to a DFA before matching, then the time complexity is going to be O(n) since every byte of input results in executing a small constant number of instructions, regardless of the size of the regex. However, DFAs typically don't handle capturing groups (although they certainly can handle grouping), with the notable exception of Laurikari's Tagged DFAs, but I don't know off-hand if the time complexity of O(n) usually associated with a DFA carries over to Tagged DFAs. Of course, the principal downside of building a DFA is that it can use exponential (in the size of the regex) memory. This is why GNU grep, rust/regex and RE2 use a hybrid approach ("lazy DFA"), which avoids O(2^n) space, but falls back to O(mn) matching when the DFA would otherwise exceed some memory budget during matching.
Well the Rust docs say "all searches execute in linear time with respect to the size of the regular expression and search text." Their engine compiles to a DFA, not an NFA or PikeVM; I suppose this is the basis for their claim.
> As another example, if you compile the regex to a DFA before matching, then the time complexity is going to be O(n) since every byte of input results in executing a small constant number of instructions, regardless of the size of the regex. However, DFAs typically don't handle capturing groups
Now you have arrived at my question! Rust compiles to a DFA that supports capture groups. My question is whether capture groups ruin the linearity of the DFA matching.
Thanks for the Laurikari's Tagged DFAs reference, I hadn't heard of that. I'll check it out!
Yeah, that's ambiguous phrasing on my part. I meant that it was linear time with respect to both the size of the regex and the search text.
> Their engine compiles to a DFA, not an NFA or PikeVM; I suppose this is the basis for their claim.
No, it doesn't. rust/regex uses some combination of the Pike VM, (bounded) backtracking and a lazy DFA. It will compile a DFA ahead of time in some cases where Aho-Corasick can be used.
> Rust compiles to a DFA that supports capture groups.
No, it uses a lazy DFA to answer "where does it match," but it still must use either the Pike VM or the bounded backtracker to resolve capture groups.
> My question is whether capture groups ruin the linearity of the DFA matching.
Yeah I think I would probably look at Tagged DFAs to answer this. You'll want to check out recent papers that cite Laurikari's work, since I think there have been some developments!
I always feel so spoiled with Rust because it seems every core "functionality" library (serde, uuid, rand, chrono, soon to be futures I hope, and innumerable more) is the best implementation of that concept in any language ever so far.
Its go weird to go from working on decade old PHP or C++ code where I constantly curse the developers for undefined or illogical behavior to Rust where I rarely ever have a hiccup - if I do, the crazy powerful documentation engine often solves it in seconds - if that doesn't work, the lints, compiler, and more and more the RLS point me in the right direction, and finally in the worst case I go actually look at the crate source and discover how well thought out the developer made things.
Its legitimate programming magic and I'm worried I'm developing a psychological dependence on Rust where in my next job if I can't get built in backtraces from my errors like Failure I might jump out a window. So thank you again (and the rest of the Rust community in general) so much for making me miserable whenever I think about having to write regular expressions (or code, in general) with any other language (except Python, even with the warts its still a sweetheart who means well, I can't stay mad at it).
(.*)\1
is not context-free.Also it's not clear if regexes with backreferences can parse XML, which is context-free.
After a lot of time I finally got the test suite to pass and was happy, naïvely thinking that "if it passes the tests it must be correct". Unfortunately I also integrated a nice case of catastrophic backtracking into the regex that timgraham fortunately caught. This could have resulted in DoS-attacks against web forms that contain validated URL fields. (This is especially nice when doing it against non-asynchronous Python servers.)
https://github.com/django/django/pull/2873
This beast was finally merged half a year later:
^(?:[a-z0-9\\.\\-])://(?:\\S+(?::\\S)?@)?(?:(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}|\\[[0-9a-f:\\.]+\\]|([a-z\u00a1-\uffff0-9](?:[a-z\u00a1-\uffff0-9-][a-z\u00a1-\uffff0-9])?(?:\\.[a-z\u00a1-\uffff0-9]+(?:[a-z\u00a1-\uffff0-9-][a-z\u00a1-\uffff0-9]+))\\.[a-z\u00a1-\uffff]{2,}\\.?|localhost))(?::\\d{2,5})?(?:[/?#][^\\s]*)?$
I like to use term regex for "regular" expressions implemented in most languages, by PCRE engine or in Perl and term regular expressions for actual regular expressions as defined in theoretical computer science, that is expressions which can be recognised by finite (either deterministic or non-deterministic) automata.
The funny thing is that they're still called regular.
There's (in more powerful order): context-free, context-sensitive, recursively-enumerable.
- dotAll mode (the s flag)
- Lookbehind assertions
- Named capture groups
- Unicode property escapes
- String.prototype.matchAll
- Legacy RegExp features
> these features aren't entirely new
Depends on what you mean by that. These features are still not universally supported by all modern browsers, for example, so I can imagine they’re still new to a lot of developers.
Chrome supports all these features (except for String#matchAll, which is currently at Stage 3). Other browsers don’t yet support the full set, but they’re all working on getting there.
var matches = [];
"12345678".replace(/\d/g, (m) => matches.push(m));
console.log(matches);You can view the implementation status of the various features here: https://kangax.github.io/compat-table/es2016plus/#test-RegEx...
Agreed! (As a regex-heavy Perl hacker, I loved the day that they entered the language.) I didn't mean to minimise them; rather quite the opposite, to point out that they gave a great return essentially for free (compared to regexes that still have capturing groups, but without names), as opposed to look-behind, which (I think) can slow down a match dramatically.
Interesting view. Is this better than a "let it break" approach?
Link rot already claims N% of websites per year. I wonder if cleaning up APIs like this one would increase N noticeably.
Or perhaps a branch and let the old stagnate approach?
Strip the deprecated features to create RegEx2/RegExNG/whatever[1] and build the new features on that. Old code can keep using the old version as they always have, new code can use all the nice new shinys, and the new version doesn't have to worry about backwards compatibility with the broken parts of dark age UAs. Also make sure everything in the new spec can be polyfilled for cases when new code needs to work on those older UAs.
[1] or while we are branching off, why not answer the complaint of regexs usually not actually being regular (as per the mathematical concept of regular languages) and rename them completely? Maybe SearchExpressions? Or SearchExp if you want something smaller. Or SExp if you want even shorter and don't mind attracting bad puns.
It's also maybe more widespread than you'd think. Adding `global` seemed safe for a long time, but ended up breaking both Flickr and Jira because they both use a library that broke: https://github.com/tc39/proposal-global/issues/20
Because that makes it significantly easier to e.g. optionally collect into an array, or to only partially iterate the sequence (e.g. only get the first 3 matches) which is painful to impossible using JS's callbacks. An iterator is simply more flexible.
[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...