Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions JSTests/stress/regexp-unicode-greedy-charclass-backtrack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Backtracking a greedy, variable-width Unicode character class must step back one code
// point in O(1). The old YARR JIT rematched from beginIndex on every backtrack step,
// turning an inherently quadratic scan into a cubic one for patterns like /[^"]*X/u on
// 16-bit input.

function assert(b, msg) {
if (!b)
throw new Error("assertion failed: " + msg);
}

function assertMatch(re, input, expected) {
var m = re.exec(input);
var got = m ? m[0] : null;
assert(got === expected, re + " on " + JSON.stringify(input) + " got " + JSON.stringify(got) + " expected " + JSON.stringify(expected));
}

// Correctness: backward step must give back exactly one code point (2 units for a valid
// surrogate pair, 1 for everything else including lone surrogates).
assertMatch(/([^"]*)Z/u, "abcZ", "abcZ");
assertMatch(/([^"]*)Z/u, "\u{1F600}abZ", "\u{1F600}abZ");
assertMatch(/([^"]*)Z/u, "ab\u{1F600}Z", "ab\u{1F600}Z");
assertMatch(/([^"]*)Z/u, "ab\u{1F600}cdZ", "ab\u{1F600}cdZ");
assertMatch(/([^"]*)Z/u, "\u{1F600}\u{1F600}Z", "\u{1F600}\u{1F600}Z");
assertMatch(/([^"]*)Z/u, "Z\u{1F600}\u{1F600}", "Z");
assertMatch(/([^"]*)Z/u, "aZ\u{1F600}b\u{1F600}", "aZ");
assertMatch(/([^"]*)Z/u, "\uDC00Z", "\uDC00Z");
assertMatch(/([^"]*)Z/u, "\uD800Z", "\uD800Z");
assertMatch(/([^"]*)Z/u, "\uD800\uD800Z", "\uD800\uD800Z");
assertMatch(/([^"]*)Z/u, "\uDC00\uDC00Z", "\uDC00\uDC00Z");
assertMatch(/([^"]*)Z/u, "\u{1F600}\uDC00Z", "\u{1F600}\uDC00Z");
assertMatch(/([^"]*)Z/u, "\uD800\u{1F600}Z", "\uD800\u{1F600}Z");
assertMatch(/([^"]*)Z/u, "\u{1F600}abc\u{1F601}", null);
assertMatch(/([^"]+)Z/u, "\u{1F600}Z", "\u{1F600}Z");
assertMatch(/([^"]+)Z/u, "Z", null);
assertMatch(/q([^"q]*)Z/u, "q\uDC00aZb", "q\uDC00aZ");
assertMatch(/q([^"q]*)Z/u, "q\uDC00aa", null);

// Alternating widths: "a😀" repeated, Z at the front so every step is unwound.
(function () {
var body = "";
for (var i = 0; i < 40; i++) body += "a\u{1F600}";
var m = /([^"]*)Z/u.exec("Z" + body);
assert(m[0] === "Z" && m[1] === "" && m.index === 0, "mixed-width full unwind");
})();

// Performance: 2000 BMP code units with one non-BMP leader. Prior to the O(1) backward
// step this ran ~2.3s (cubic); now it matches the interpreter at ~10ms.
(function () {
var body = "";
for (var i = 0; i < 2000; i++) body += "a";
var s = "\u2014" + body;
var start = preciseTime();
assert(/[^"]*X/u.test(s) === false, "no match expected");
var dt = preciseTime() - start;
assert(dt < 1.0, "cubic backtracking regression: " + dt + "s");
})();
40 changes: 24 additions & 16 deletions Source/JavaScriptCore/yarr/YarrJIT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3394,29 +3394,37 @@
else if (term->isFixedWidthCharacterClass())
m_jit.sub32(MacroAssembler::TrustedImm32(term->characterClass->hasNonBMPCharacters() ? 2 : 1), m_regs.index);
else {
// Rematch one less
#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS)
// Unmatch one code point in O(1) by stepping backward. The forward loop advanced
// index by 2 only for a valid surrogate pair (tryReadUnicodeChar combines them) and
// by 1 otherwise, so the last matched code point spans two code units iff the unit
// at input[index - 1] is a trail surrogate preceded by a lead surrogate inside the
// range covered by this term (beginIndex was stored in generateCharacterClassGreedy).
const MacroAssembler::RegisterID character = m_regs.regT0;
const MacroAssembler::RegisterID scratch = m_regs.regT2;
m_usesT2 = true;

loadFromFrame(term->frameLocation + BackTrackInfoCharacterClass::beginIndex(), m_regs.index);
m_jit.sub32(MacroAssembler::TrustedImm32(1), m_regs.index);

MacroAssembler::Label rematchLoop(&m_jit);
MacroAssembler::Jump doneRematching = m_jit.branchTest32(MacroAssembler::Zero, countRegister);
m_jit.load16Unaligned(negativeOffsetIndexedAddress(op.m_checkedOffset - term->inputPosition, character), character);
m_jit.and32(MacroAssembler::TrustedImm32(0xfc00), character);
MacroAssembler::Jump notTrail = m_jit.branch32(MacroAssembler::NotEqual, character, MacroAssembler::TrustedImm32(0xdc00));

readCharacter(op.m_checkedOffset - term->inputPosition, character);
loadFromFrame(term->frameLocation + BackTrackInfoCharacterClass::beginIndex(), scratch);
MacroAssembler::Jump atBegin = m_jit.branch32(MacroAssembler::BelowOrEqual, m_regs.index, scratch);

m_jit.sub32(MacroAssembler::TrustedImm32(1), countRegister);
m_jit.add32(MacroAssembler::TrustedImm32(1), m_regs.index);

#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS)
MacroAssembler::Jump isBMPChar = m_jit.branch32(MacroAssembler::LessThan, character, MacroAssembler::TrustedImm32(0x10000));
m_jit.add32(MacroAssembler::TrustedImm32(1), m_regs.index);
isBMPChar.link(&m_jit);
#endif
m_jit.load16Unaligned(negativeOffsetIndexedAddress((op.m_checkedOffset - term->inputPosition) + 1u, character), character);
m_jit.and32(MacroAssembler::TrustedImm32(0xfc00), character);
MacroAssembler::Jump notLead = m_jit.branch32(MacroAssembler::NotEqual, character, MacroAssembler::TrustedImm32(0xd800));

Check warning on line 3418 in Source/JavaScriptCore/yarr/YarrJIT.cpp

View check run for this annotation

Claude / Claude Code Review

Backward-step mask (0xfc00) inconsistent with forward tryReadUnicodeChar mask (0xdc00) — index/count desync

The backward step masks with `0xfc00` (correct per Unicode), but the forward loop's `tryReadUnicodeCharImpl` masks with `surrogateTagMask = 0xdc00dc00` (pre-existing bug that drops bit 13), so it bogus-combines pairs like `[U+F800, U+FC00]` and advances 2 units for one count — the new backward step then retreats only 1, desyncing index/count where the old rematch loop stayed self-consistent. E.g. `/[^"]*X/u.exec("X\uF800\uFC00")` now returns `null` in the JIT where the old JIT (and interpreter/s
Comment thread
robobun marked this conversation as resolved.
Resolved

m_jit.jump(rematchLoop);
doneRematching.link(&m_jit);
m_jit.sub32(MacroAssembler::TrustedImm32(1), m_regs.index);

loadFromFrame(term->frameLocation + BackTrackInfoCharacterClass::matchAmountIndex(), countRegister);
notTrail.link(&m_jit);
atBegin.link(&m_jit);
notLead.link(&m_jit);
#else
m_jit.sub32(MacroAssembler::TrustedImm32(1), m_regs.index);
#endif
}
m_jit.jump(op.m_reentry);
}
Expand Down
Loading