Prefer .NET Regex for quantified groups without capture or lookaround hazards#2682
Merged
Conversation
lahma
enabled auto-merge (squash)
July 14, 2026 07:08
… hazards RegExpConstructor.NeedCustomEngine routed every quantified group whose body contained any quantifier to the custom backtracking interpreter, and the '?' of a '(?:' group opener was itself scanned as a quantifier, so every quantified non-capturing group fell back too. ETW profiling showed the custom interpreter at 9.1% self time on string-tagcloud, whose two hot json2.js patterns (the token alternation with '(?:\.\d*)?' and '(?:\s*\[)+') are both .NET-translatable with identical semantics. Replace the quantifier-poisoning heuristic with an analysis of the actual .NET vs ECMAScript divergences (RepeatMatcher semantics): - a quantified group needs the custom engine only when its body contains a capturing group or lookaround assertion (at any depth) - .NET retains captures from earlier iterations while ECMAScript clears them per iteration; this also closes a hole where '((a)|b)+'-style patterns routed to .NET and returned non-spec captures, - a quantified capturing group additionally needs the custom engine when its body can match the empty string - .NET records an empty capture for an empty iteration while ECMAScript rejects empty iterations; nullability is tracked per alternation branch, which also fixes '(a|)*' and '(\b)*' previously routing to .NET with non-spec group results, - quantified lookarounds keep falling back, backreference, named-group, unicode-mode and IgnoreCase rules are unchanged, inline 'i' modifiers now fall back even when preceded by other modifiers, and 'm'/'s' modifier groups no longer fall back since Acornima adapts them compliantly. string-tagcloud's hot patterns now route to .NET Regex with verified identical match results and lastIndex behavior on both engines; string-unpack-code already routed all of its patterns to .NET. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013cnj7xfz8iZ5uaeU82keKa
lahma
force-pushed
the
perf-regexp-routing
branch
from
July 14, 2026 07:19
4b78139 to
81a8605
Compare
This was referenced Jul 14, 2026
Merged
lahma
added a commit
that referenced
this pull request
Jul 15, 2026
… Regex (#2694) RegExpConstructor.NeedCustomEngine (#2682) diverted a quantified group to the custom engine only when its body captured/looked-around or was a nullable *capturing* group. A non-capturing quantified group whose body can reach the empty string before a consuming alternative was sent to .NET Regex, whose empty-subexpression loop protection stops the repetition at the first empty or zero-width alternative. ECMAScript's RepeatMatcher instead prunes the empty iteration and backtracks into a later consuming alternative, so the match itself diverged (silently truncated): /(?:a*|b)*/.exec("aaabbb") spec "aaabbb", was "aaa" /(?:|a)*/.exec("aaa") spec "aaa", was "" "aaa".replace(/(?:|a)*/g, "X") spec "XX", was "XaXaXaX" This regressed v4.12.0: its (?:-header-skip bug happened to route every (?:...)quantifier group to the custom engine, and #2682's correct header skip exposed the gap. Fix: route any nullable-bodied quantified group (capturing or not) to the custom engine. Conservative - some single greedy-nullable bodies like (?:a*)+ would in fact agree with .NET - but correctness first; the hot benchmark patterns (non-nullable bodies) are unaffected. Claude-Session: https://claude.ai/code/session_01XM8rSnn8j66kDCTaP2exNK Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ETW profiling showed Jint's custom backtracking RegExp interpreter (
RegExpInterpreter.ExecBacktrack) at 9.1% self time on string-tagcloud — in a script whose patterns are all .NET-translatable. Root cause: the quantified-group hazard scan inNeedCustomEnginetreated the?of every(?:group opener as a quantifier inside the group body, so every quantified non-capturing group ((?:x)+,(?:\s*\[)+, …) — one of the most common pattern shapes — routed to the interpreter. Meanwhile the check missed the hazard it was written for:((a)|b)+routed to .NET Regex and returned non-spec capture results on main.The scan is rewritten with per-group state (capture/lookaround body flags, per-alternation-branch nullability): a quantified group now bails to the interpreter only when its body contains a capture or lookaround at any depth, or the group itself captures and can match empty (.NET keeps prior-iteration/empty-iteration captures where ECMAScript clears/rejects them — divergences verified against raw .NET behavior). Escape atoms (
\uXXXX,\xHH,\cX, octal) are consumed whole so quantifiers attach to the right atom, andm/sinline modifier groups no longer bail (Acornima's adaptation verified compliant including\rLineTerminator handling).Correctness fixes riding along (all were wrong on main via .NET routing, now spec-correct and tested):
((a)|b)+,(a|)*,(\b)*.Verified with a 49-case routing matrix, a dual-engine parity script (identical match arrays, indices and
lastIndexbehavior including sticky/global), and 10 new tests.Benchmarks (default jobs, adjacent A/B on latest main)
Broad effect: any user pattern with a quantified non-capturing group that previously fell to the interpreter via the
(?:mis-scan now runs on .NET Regex when otherwise safe.Gates
🤖 Generated with Claude Code