DEV Community

FF
FF

Posted on Originally published at regextester.dev

One regex, seven engines: catch portability bugs before you ship

"Regular expressions" is not one language. It's a family of dialects that look almost identical and disagree in exactly the places you don't look until production breaks:

  • \w is Unicode-aware by default in Python 3, PCRE2 and .NET — but ASCII-only in JavaScript.
  • Lookbehind must be fixed-width in PCRE2; .NET accepts variable width; RE2-based engines reject it entirely.
  • Possessive quantifiers, atomic groups, \K, named-group syntax (?<name>…) vs (?P<name>…) — every one of these is a line in some engine's grammar and an error (or a silent behavior change) in another's.

The standard workflow makes this invisible. You test the pattern in one tester with one flavor selected, it works, you paste it into another language — and the failure shows up far away from the regex: a log line that never matches, a validation that rejects valid input, an import script that silently skips rows.

A flavor selector doesn't solve this either — you'd still have to try every flavor by hand and compare results from memory. What you need is every engine answering at once, side by side, on the same input. That's what the Compat button in RegexTester.dev does.

Seven engine paths, one click

Type your pattern and a representative test string, then hit the Compat button in the toolbar — or open the deep link regextester.dev/compat directly.

Compat toolbar

One pattern runs through seven execution paths in parallel:

  1. PCRE2 — real engine, compiled to WebAssembly
  2. Go (regexp) — real engine, WebAssembly
  3. Rust (regex) — real engine, WebAssembly
  4. JavaScript — the browser's native RegExp
  5. .NET — a real interpreter for System.Text.RegularExpressions syntax
  6. Python — parser plus normalize-and-run path
  7. Java — parser plus normalize-and-run path

Not simulated: the WASM paths are the actual engine builds, so a rejection or a match here is what the real engine does.

The three ways a port fails

Every engine reports one of three outcomes, and the panel clusters engines by their exact match sets:

  • Rejection — the pattern doesn't even compile. RE2's rejection of lookbehind, or a possessive quantifier a dialect has never heard of, is a build error you want to know about before the port.

Rejection

  • Silent divergence — the worst one. The pattern compiles everywhere and simply disagrees. Classic example: \w+ against Unicode input gives JS and Java four matches where the other five engines give two, because \w is ASCII-only there. No error, no warning — different data.

Divergence

  • Full agreement — all seven engines return identical match sets (text, position, groups), and the panel says so explicitly.

Fully compatible

Exporting the verdict

The verdict copy is written to be pasted: one click copies a Markdown report listing every engine, its outcome, and the divergent match sets — drop it into a PR when you're porting a pattern between services written in different languages.


Try it: open regextester.dev/compat, paste the pattern you're about to port, and see all seven verdicts in a couple of seconds.

Full write-up: regextester.dev/blog/regex-compatibility-checker. Free, no signup, everything runs locally in your browser.

Top comments (0)