RegExp

Cruft's own regular-expression engine and how much of the JavaScript RegExp surface it supports: named groups, named and numbered backreferences, variable-length lookbehind, Unicode property escapes, and the full flag set including v-mode set operations. There is no catastrophic-backtracking guard, so a pathological pattern hangs.

Cruft's regular-expression engine is its own, distinct from both V8's Irregexp and the Rust regex crate. That choice is deliberate: the regex crate is a linear-time automaton that structurally cannot support backreferences or lookbehind, and JavaScript regular expressions require both. Cruft therefore ships its own backtracking matcher with its own Unicode property database, so that ECMA-262 patterns behave the way the ecosystem expects.

The surface is broad and correct. Named groups, numbered and named backreferences, fixed- and variable-length lookbehind, the full flag set (g i m s u y d v), Unicode property escapes, and v-flag set operations all work. The two boundaries worth knowing are that \q{...} string literals only work inside a character class (as the spec intends; a standalone use correctly throws), and that there is no catastrophic-backtracking guard: a pathological pattern will hang, exactly as it does under V8.

Feature status

FeatureStatusNotes
Named groups (?<n>…) + .groupsWorks
Named backref \k<n>Works
Numbered backref \1WorksAlso inside lookbehind
Lookahead (?=) / (?!)Works
Lookbehind fixed (?<=ab)Works
Lookbehind variable-length (?<=a+)WorksFull backtracking, not fixed-width only
Negative lookbehind (?<!…)Works
Unicode property escapes \p{…} / \P{…}WorksBroad: general categories, scripts, script extensions, binary properties. See below
Flags g i m s y dWorks
Flag u (astral, \u{…}, full case-fold)Works
Flag v (unicode sets)Works
v set intersection &&, subtraction --, nestingWorks
v string literal \q{…}WorksCorrect inside a class; throws (per spec) when used standalone
.indices (d flag) + .indices.groupsWorks
Sticky y lastIndex semanticsWorks
matchAll, replaceAll, split with capturesWorks
replace $1 / lt;name> / function replacerWorks
Catastrophic-backtracking guardAbsentPathological patterns hang (as in V8)

Named groups, backreferences

Named capture groups populate .groups, and both named and numbered backreferences resolve during matching:

$ cruft -e 'const m="2026-07-25".match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/);console.log(JSON.stringify(m.groups))'
{"y":"2026","m":"07","d":"25"}
$ cruft -e 'console.log(/(?<q>["\x27]).*?\k<q>/.test(`"hi"`), /(?<q>["\x27]).*?\k<q>/.test(`"hi\x27`))'
true false
$ cruft -e 'console.log(/(\w)\1/.test("aa"), /(\w)\1/.test("ab"))'
true false

Lookahead and lookbehind

Lookahead in both polarities works. Lookbehind is a common failure point for independent engines because a backtracking matcher has to run the sub-pattern in reverse; Cruft handles both the fixed-width and the variable-length case, including a backreference captured inside the lookbehind.

$ cruft -e 'console.log(/foo(?=bar)/.test("foobar"), /foo(?!bar)/.test("foobaz"))'
true true
$ cruft -e 'console.log("$5".match(/(?<=\$)\d+/)?.[0])'
5
$ cruft -e 'console.log("aaab".match(/(?<=a+)b/)?.[0])'          # variable-length
b
$ cruft -e 'console.log("1px".match(/(?<!\d)px/), "apx".match(/(?<!\d)px/)?.[0])'
null px
$ cruft -e 'console.log("aax".match(/(?<=(\w)\1)x/)?.[0])'       # backref inside lookbehind
x

Unicode property escapes

Property escapes require the u (or v) flag. Coverage is broad. General categories, their long forms, scripts, Script_Extensions, and a wide range of binary properties all resolve, and they resolve correctly: they reject the wrong character as well as accepting the right one:

$ cruft -e 'console.log(/\p{L}/u.test("a"), /\p{Nd}/u.test("5"), /\p{P}/u.test("."))'
true true true
$ cruft -e 'console.log(/\p{Lu}/u.test("A"), /\p{Ll}/u.test("a"), /\p{Ll}/u.test("A"))'
true true false
$ cruft -e 'console.log(/\p{Emoji}/u.test("😀"), /\p{Emoji}/u.test("a"))'
true false
$ cruft -e 'console.log(/\p{Script=Greek}/u.test("α"), /\p{Script=Greek}/u.test("a"))'
true false
$ cruft -e 'console.log(/\p{Script=Cyrillic}/u.test("д"), /\p{Script=Han}/u.test("中"))'
true true
$ cruft -e 'console.log(/\p{General_Category=Letter}/u.test("a"), /\p{Script_Extensions=Greek}/u.test("α"))'
true true
$ cruft -e 'console.log(/\P{L}/u.test("5"), /\P{L}/u.test("a"))'   # negated
true false

Long and short property names both work (\p{Script=Greek} and \p{sc=Greek}, \p{General_Category=Nd} and \p{gc=Nd}, \p{Hex_Digit}, \p{White_Space}, \p{Ideographic}, and so on). An unknown property name is a syntax error at compile time, which is the correct behavior:

$ cruft -e '/\p{NotARealProperty}/u.test("a")'
cruft: evaluation error: SyntaxError: compile: parse: invalid Unicode property escape `NotARealProperty`

Property-escape boundary: the shipped database covers the commonly-used categories, scripts, script extensions, and binary properties shown above. It is a curated table rather than a full ICU property dump, so a very obscure property or a freshly-added Unicode value may not be present. Verify the specific property you depend on; a missing one fails closed at compile time (a SyntaxError), never as a silent mis-match.

Flags

Every ECMA-262 flag is present. The two that most often reveal engine bugs are y (sticky, which must anchor at lastIndex and reset it precisely) and d (which must emit correct [start, end] index pairs, including for named groups):

$ cruft -e 'console.log(/a.b/s.test("a\nb"), /a.b/.test("a\nb"))'   # s dotall
true false
$ cruft -e 'console.log("a\nb".match(/^b/m)?.[0])'                  # m multiline
b
$ cruft -e 'const r=/\d/y; r.lastIndex=2; console.log(r.test("ab3"), r.lastIndex)'
true 3
$ cruft -e 'const r=/\d/y; r.lastIndex=0; console.log(r.test("a3"), r.lastIndex)'  # sticky miss resets
false 0
$ cruft -e 'const m=/(?<yr>\d{4})/d.exec("2026"); console.log(JSON.stringify(m.indices), JSON.stringify(m.indices.groups))'
[[0,4],[0,4]] {"yr":[0,4]}

The u flag brings full astral support: . matches a whole code point, \u{…} escapes work, and case-folding under i + u is genuine Unicode folding, not ASCII-only (the Kelvin sign U+212A folds to k, Σ folds to σ):

$ cruft -e 'console.log(/^.$/u.test("😀"), /^.$/.test("😀"))'
true false
$ cruft -e 'console.log(/\u{1F600}/u.test("😀"))'
true
$ cruft -e 'console.log(/K/iu.test("k"), /σ/iu.test("Σ"))'
true true

The v flag and set operations

The v (unicode sets) flag works, and so do its set operations. Intersection (&&), subtraction (--), and nested classes all evaluate correctly:

$ cruft -e 'console.log(/^[\p{L}&&[a-z]]$/v.test("m"), /^[\p{L}&&[a-z]]$/v.test("A"))'
true false
$ cruft -e 'console.log(/^[\p{L}--[aeiou]]$/v.test("b"), /^[\p{L}--[aeiou]]$/v.test("a"))'
true false
$ cruft -e 'console.log(/^[[a-z]&&[^aeiou]]$/v.test("b"), /^[[a-z]&&[^aeiou]]$/v.test("a"))'
true false

\q{…} string literals behave to spec. Inside a character class, where the spec allows them, they work correctly, including multi-character alternatives and longest-match preference:

$ cruft -e 'console.log("xy".match(/[\q{abc|xy}]/v)?.[0])'
xy
$ cruft -e 'console.log("abc".match(/[\q{a|abc}]/v)?.[0])'   # prefers the longer string
abc
$ cruft -e 'console.log("d".match(/[\q{abc}d]/v)?.[0], "abc".match(/[\q{abc}d]/v)?.[0])'
d abc

Used outside a character class, where the spec makes \q{…} a SyntaxError, Cruft rejects it too:

$ cruft -e "console.log(/\q{abc}/v.test('abc'))"
cruft: evaluation error: SyntaxError: compile: parse: `\q{}` is only valid inside a `v`-mode character class

So \q{…} is class-interior only, and a standalone use throws at compile time rather than silently matching.

String integration

matchAll, replaceAll, split, and the replace replacement grammar all integrate with the engine, including named-group substitution and function replacers that receive the groups object:

$ cruft -e 'console.log([...("a1b2".matchAll(/(?<L>[a-z])(?<N>\d)/g))].map(m=>m.groups.L+m.groups.N))'
[ 'a1', 'b2' ]
$ cruft -e 'console.log("2026-07-25".replaceAll(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/g, "lt;d>/lt;m>/lt;y>"))'
25/07/2026
$ cruft -e 'console.log("a-b c-d".replaceAll(/(\w)-(\w)/g, "$2$1"))'
ba dc
$ cruft -e 'console.log(JSON.stringify("a1b2c".split(/(\d)/)))'   # capturing split keeps delimiters
["a","1","b","2","c"]
$ cruft -e 'console.log("2026-07".replace(/(?<y>\d+)-(?<m>\d+)/, (...a)=>{const g=a.at(-1); return g.m+"/"+g.y}))'
07/2026

Performance and backtracking

Linear patterns over large input are fast; a 5 MB haystack scans in a few milliseconds:

$ cruft -e 'const s="x".repeat(5e6)+"needle"; const t0=Date.now(); console.log(/needle/.test(s), Date.now()-t0+"ms")'
true 16ms

Because this is a backtracking engine, it is vulnerable to catastrophic backtracking, and there is no built-in step budget or timeout. A pathological pattern hangs the call:

$ cruft -e 'console.log(/(a+)+$/.test("a".repeat(40)+"b"))'   # does not return

This matches V8's behavior on the same input (classic ReDoS hangs there too), so it is not a Cruft-specific defect. But Cruft offers no automatic escape hatch. If you match untrusted input against untrusted or complex patterns, budget the work externally (for example, run it inside a worker or a compartment with a wall-clock bound) rather than relying on the engine to bail out.