Skip to content

Fix sticky+global [Symbol.match] .NET fast path returning wrong results#2600

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:fix-sticky-global-match
Jul 9, 2026
Merged

Fix sticky+global [Symbol.match] .NET fast path returning wrong results#2600
lahma merged 2 commits into
sebastienros:mainfrom
lahma:fix-sticky-global-match

Conversation

@lahma

@lahma lahma commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes the sticky+global [Symbol.match] .NET fast path returning wrong results:

"aaa".match(/a/gy)     // was ["a","a"]      → now ["a","a","a"]
"ababab".match(/ab/gy) // was ["ab"]         → now ["ab","ab","ab"]
"aab".match(/a*/gy)    // was ["aa"]         → now ["aa","",""]

Why

The sticky branch in RegExpPrototype.Match compared match.Index (a string position) against
++li (a match counter), and called SetLength(li) after having written array index li:

  • any match wider than one character made the position ≠ counter comparison break the loop after the
    first match, and
  • even for single-character matches the final length truncated the last written element.

How

The loop now tracks the expected continuation position — previous match end, or +1 for empty matches
(AdvanceStringIndex; this fast path is guarded !fullUnicode, so the advance is always one UTF-16
unit) — and sets the array length to the number of matches written. lastIndex semantics are already
covered by the rx.Set(lastIndex, +0) at the top of Match (final observable value matches the spec's
failing-exec reset).

Tests

Test262 doesn't cover sticky+global [Symbol.match] through the .NET engine path, so this adds
regression tests (each shape runs against both the .NET fast path and the generic exec loop via the
u flag):

  • adjacent single- and multi-char matches collected fully
  • termination at the first non-adjacent position (later matches must not be included)
  • empty-match advancement (/a*/gy)
  • lastIndex reset to 0 after the call

Gating

  • dotnet build Jint/Jint.csproj -c Release (all TFMs): ✅
  • Jint.Tests net10.0 + net472: ✅
  • Jint.Tests.PublicInterface net10.0 + net472: ✅
  • Full Test262: ✅ (99,260 passed / 0 failed)

🤖 Generated with Claude Code

The sticky branch compared match.Index (a string position) against a
match counter and set the array length to the counter after writing
index counter, so:

- "aaa".match(/a/gy) returned ["a","a"] instead of ["a","a","a"]
  (off-by-one truncation), and
- "ababab".match(/ab/gy) returned ["ab"] instead of ["ab","ab","ab"]
  (any match wider than one character broke the position/counter
  comparison).

Track the expected continuation position (previous match end, +1 for
empty matches per AdvanceStringIndex - this fast path is never unicode)
and set the array length to the number of matches written. Regression
tests cover adjacent multi-char matches, gap termination, empty-match
advancement and lastIndex reset, each against both the .NET fast path
and the generic exec loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) July 9, 2026 05:51
@lahma
lahma merged commit 3eada0f into sebastienros:main Jul 9, 2026
4 checks passed
lahma added a commit that referenced this pull request Jul 9, 2026
Full re-run after the 2026-07 interpreter-parity campaign
(#2600-#2606, #2608-#2611). Highlights vs the 2026-07-07 refresh:
dromaeo-core-eval -40% (1.52 -> 0.92 ms, taking first place from
NiL.JS), dromaeo-3d-cube -19% time / -35% allocation (7.38 -> 5.99 ms,
moving ahead of NiL.JS - only the IL-compiling engine remains faster),
stopwatch -7% (tied with the best interpreter, ahead of Jurassic),
stopwatch-modern -13% (tied with Jurassic), object-array -10%,
object-regexp -13%, object-string -5%. Competitor versions unchanged;
their rows served as thermal canaries (all within a few percent of the
previous refresh).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma deleted the fix-sticky-global-match branch July 13, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant