Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
96 changes: 96 additions & 0 deletions JSTests/stress/regexp-ignore-case-unicode-16-17-case-pairs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//@ runDefault("--useRegExpJIT=true")
//@ runNoJIT("--useRegExpJIT=false")

// Non-unicode /i matching uses the committed table in YarrCanonicalizeUCS2.cpp.
// That table has to contain the BMP simple case pairs added in Unicode 16 and 17;
// /iu (generated from ucd/CaseFolding.txt at build time) already had them.

function shouldBe(actual, expected, message) {
if (actual !== expected)
throw new Error(message + ": expected " + expected + " but got " + actual);
}

function escapeForClass(ch) {
return "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
}

function checkPair(lower, upper, flags) {
const lo = String.fromCharCode(lower);
const up = String.fromCharCode(upper);
const name = "U+" + lower.toString(16) + "/U+" + upper.toString(16) + " /" + flags;

for (const [pattern, input] of [[lo, up], [up, lo]]) {
const escaped = escapeForClass(pattern);
shouldBe(new RegExp(escaped, flags).test(input), true, name + " atom");
shouldBe(new RegExp("^" + escaped + "$", flags).test(input), true, name + " anchored atom");
shouldBe(new RegExp("[" + escaped + "]", flags).test(input), true, name + " class");
shouldBe(new RegExp("[^" + escaped + "]", flags).test(input), false, name + " negated class");
shouldBe(new RegExp("x" + escaped + "y", flags).test("x" + input + "y"), true, name + " inside a longer atom");
shouldBe(new RegExp("(" + escaped + ")\\1", flags).test(pattern + input), true, name + " backreference");
shouldBe(("a" + input + "b").replace(new RegExp(escaped, flags), "-"), "a-b", name + " replace");
}
}

function checkUnrelated(a, b, flags) {
const name = "U+" + a.toString(16) + " vs U+" + b.toString(16) + " /" + flags;
shouldBe(new RegExp(escapeForClass(String.fromCharCode(a)), flags).test(String.fromCharCode(b)), false, name + " atom");
shouldBe(new RegExp("[" + escapeForClass(String.fromCharCode(a)) + "]", flags).test(String.fromCharCode(b)), false, name + " class");
}

// [lower, upper]
const newPairs = [
// Unicode 16
[0x019b, 0xa7dc], // LATIN LETTER LAMBDA WITH STROKE
[0x0264, 0xa7cb], // LATIN LETTER RAMS HORN
[0xa7cd, 0xa7cc], // LATIN LETTER S WITH DIAGONAL STROKE
[0xa7db, 0xa7da], // LATIN LETTER LAMBDA
[0x1c8a, 0x1c89], // CYRILLIC LETTER TJE
// Unicode 17
[0xa7cf, 0xa7ce], // LATIN LETTER PHARYNGEAL VOICED FRICATIVE
[0xa7d3, 0xa7d2], // LATIN LETTER DOUBLE THORN (capital is new, the small letter is from Unicode 14)
[0xa7d5, 0xa7d4], // LATIN LETTER DOUBLE WYNN (same)
];

// Pairs next to the new entries whose table ranges were merged or split by the update.
const neighbouringPairs = [
[0x019a, 0x023d], // LATIN LETTER L WITH BAR
[0x0263, 0x0194], // LATIN LETTER GAMMA
[0x0265, 0xa78d], // LATIN LETTER TURNED H
[0xa7d1, 0xa7d0], // LATIN LETTER CLOSED INSULAR G
[0xa7d7, 0xa7d6], // LATIN LETTER MIDDLE SCOTS S
[0xa7d9, 0xa7d8], // LATIN LETTER SIGMOID S
[0xa7f6, 0xa7f5], // LATIN LETTER REVERSED HALF H
[0xa64b, 0xa64a], // CYRILLIC LETTER MONOGRAPH UK (also equivalent to U+1C88 below)
];

for (let i = 0; i < 50; ++i) {

Check warning on line 66 in JSTests/stress/regexp-ignore-case-unicode-16-17-case-pairs.js

View check run for this annotation

Claude / Claude Code Review

Use testLoopCount instead of hardcoded iteration count

Nit: `JSTests/README.md` (rule #2, imported by `JSTests/CLAUDE.md`) requires new tests to use `testLoopCount` for the iteration count so the harness can tune it per configuration — under `//@ runNoJIT` the fixed 50 iterations are wasted, and other configs may want a different tier-up count. Since the PR notes this file also runs on V8, guard it: `for (let i = 0; i < (typeof testLoopCount !== "undefined" ? testLoopCount : 50); ++i)`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: JSTests/README.md (rule #2, imported by JSTests/CLAUDE.md) requires new tests to use testLoopCount for the iteration count so the harness can tune it per configuration — under //@ runNoJIT the fixed 50 iterations are wasted, and other configs may want a different tier-up count. Since the PR notes this file also runs on V8, guard it: for (let i = 0; i < (typeof testLoopCount !== "undefined" ? testLoopCount : 50); ++i).

Extended reasoning...

What this is

JSTests/README.md line 20 (which JSTests/CLAUDE.md pulls in via @README.md) states that new tests are required to:

Use testLoopCount or wasmTestLoopCount to control how many iterations a test runs. The jsc CLI sets these based on the configuration of the test, so tests iterate enough to tier up where that matters and exit early where it doesn't.

The new test at JSTests/stress/regexp-ignore-case-unicode-16-17-case-pairs.js:66 instead uses a hardcoded loop bound:

for (let i = 0; i < 50; ++i) {

This is the repository's stated convention for new stress tests (2000+ existing files under JSTests/stress use testLoopCount), so flagging it here even though it has no functional effect on what the test verifies.

Why it matters (mildly)

This test declares two configurations at the top:

//@ runDefault("--useRegExpJIT=true")
//@ runNoJIT("--useRegExpJIT=false")

Under runNoJIT, nothing tiers up, so 50 iterations of the full pair/neighbour/unrelated battery just repeat the same interpreter path 50× for no additional coverage — pure wasted wall-clock in a suite whose README also asks tests to stay under 200ms per configuration. Under runDefault, 50 may or may not line up with the Yarr/baseline tier-up thresholds the harness would otherwise pick. testLoopCount exists precisely so the harness can set the count high where tier-up matters and low (or 1) where it doesn't.

Step-by-step

  1. run-jsc-stress-tests launches the runNoJIT configuration and injects a small testLoopCount (since JIT is disabled, extra iterations add nothing).
  2. The test ignores that value and runs the outer loop 50 times anyway.
  3. Each iteration executes 8 new pairs × 2 flag sets × 2 directions × 7 assertions, plus 8 neighbouring pairs × the same, plus the U+1C88 and 7 checkUnrelated checks — several thousand RegExp constructions and matches per iteration, ~50× more work than the harness asked for in that configuration.
  4. Under runDefault, if the harness would have chosen a testLoopCount higher than 50 to guarantee tier-up of the compiled patterns, the hardcoded 50 may undershoot instead.

Neither of these makes the test wrong — the assertions still cover the fix and fail on the old table — it just doesn't let the harness tune the cost/coverage trade-off the way the README requires.

Why the guard

The PR description says the test "passes on V8 as written," and V8's d8 shell does not define testLoopCount. A bare for (let i = 0; i < testLoopCount; ++i) would throw ReferenceError there. Guarding with typeof testLoopCount !== "undefined" ? testLoopCount : 50 keeps the file portable to V8 while honouring the JSTests convention under jsc.

Suggested fix

for (let i = 0; i < (typeof testLoopCount !== "undefined" ? testLoopCount : 50); ++i) {

Severity: nit — this is a test-suite convention for new tests, not a correctness issue; the test already validates the table change correctly.

for (const [lower, upper] of newPairs) {
checkPair(lower, upper, "i");
checkPair(lower, upper, "iu");
}
for (const [lower, upper] of neighbouringPairs) {
checkPair(lower, upper, "i");
checkPair(lower, upper, "iu");
}

// U+1C88 CYRILLIC SMALL LETTER UNBLENDED UK is in the same set as U+A64A/U+A64B.
checkPair(0x1c88, 0xa64a, "i");
checkPair(0x1c88, 0xa64b, "i");

// The new pairs must not leak into the code units around them.
checkUnrelated(0x1c8a, 0x1c8b, "i"); // U+1C8B is unassigned
checkUnrelated(0x1c89, 0x1c88, "i");
checkUnrelated(0xa7cb, 0xa7ca, "i"); // U+A7CA pairs with U+A7C9
checkUnrelated(0xa7dc, 0xa7dd, "i"); // U+A7DD is unassigned
checkUnrelated(0xa7dc, 0xa7db, "i");
checkUnrelated(0x019b, 0x019a, "i");
checkUnrelated(0x0264, 0x0263, "i");
}

// Case-insensitive ranges in classes cover the new partners too. Each tested
// character lies outside the range and can only match through its partner.
shouldBe(/[\ua7cc-\ua7da]/i.test("\ua7db"), true, "U+A7DB matches through U+A7DA at the end of the range");
shouldBe(/[\u0190-\u01a0]/i.test("\ua7dc"), true, "U+A7DC matches through U+019B inside the range");
shouldBe(/[\ua7c0-\ua7ff]/i.test("\u019b"), true, "U+019B matches through U+A7DC inside the range");
shouldBe(/[\ua7c0-\ua7ff]/i.test("\u0264"), true, "U+0264 matches through U+A7CB inside the range");
shouldBe(/[\u1c80-\u1c89]/i.test("\u1c8a"), true, "U+1C8A matches through U+1C89 at the end of the range");
16 changes: 8 additions & 8 deletions Source/JavaScriptCore/yarr/YarrCanonicalizeUCS2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ constinit const CanonicalizationRange ucs2RangeInfo[ucs2CanonicalizationRanges]
{ 0x0197, 0x0197, 0x00d1, CanonicalizeRangeLo },
{ 0x0198, 0x0199, 0x0000, CanonicalizeAlternatingAligned },
{ 0x019a, 0x019a, 0x00a3, CanonicalizeRangeLo },
{ 0x019b, 0x019b, 0x0000, CanonicalizeUnique },
{ 0x019b, 0x019b, 0xa641, CanonicalizeRangeLo },
{ 0x019c, 0x019c, 0x00d3, CanonicalizeRangeLo },
{ 0x019d, 0x019d, 0x00d5, CanonicalizeRangeLo },
{ 0x019e, 0x019e, 0x0082, CanonicalizeRangeLo },
Expand Down Expand Up @@ -193,7 +193,7 @@ constinit const CanonicalizationRange ucs2RangeInfo[ucs2CanonicalizationRanges]
{ 0x0261, 0x0261, 0xa54b, CanonicalizeRangeLo },
{ 0x0262, 0x0262, 0x0000, CanonicalizeUnique },
{ 0x0263, 0x0263, 0x00cf, CanonicalizeRangeHi },
{ 0x0264, 0x0264, 0x0000, CanonicalizeUnique },
{ 0x0264, 0x0264, 0xa567, CanonicalizeRangeLo },
{ 0x0265, 0x0265, 0xa528, CanonicalizeRangeLo },
{ 0x0266, 0x0266, 0xa544, CanonicalizeRangeLo },
{ 0x0267, 0x0267, 0x0000, CanonicalizeUnique },
Expand Down Expand Up @@ -369,7 +369,8 @@ constinit const CanonicalizationRange ucs2RangeInfo[ucs2CanonicalizationRanges]
{ 0x1c86, 0x1c86, 0x0013, CanonicalizeSet },
{ 0x1c87, 0x1c87, 0x0014, CanonicalizeSet },
{ 0x1c88, 0x1c88, 0x0016, CanonicalizeSet },
{ 0x1c89, 0x1c8f, 0x0000, CanonicalizeUnique },
{ 0x1c89, 0x1c8a, 0x0000, CanonicalizeAlternatingUnaligned },
{ 0x1c8b, 0x1c8f, 0x0000, CanonicalizeUnique },
{ 0x1c90, 0x1cba, 0x0bc0, CanonicalizeRangeHi },
{ 0x1cbb, 0x1cbc, 0x0000, CanonicalizeUnique },
{ 0x1cbd, 0x1cbf, 0x0bc0, CanonicalizeRangeHi },
Expand Down Expand Up @@ -529,11 +530,10 @@ constinit const CanonicalizationRange ucs2RangeInfo[ucs2CanonicalizationRanges]
{ 0xa7c5, 0xa7c5, 0xa543, CanonicalizeRangeHi },
{ 0xa7c6, 0xa7c6, 0x8a38, CanonicalizeRangeHi },
{ 0xa7c7, 0xa7ca, 0x0000, CanonicalizeAlternatingUnaligned },
{ 0xa7cb, 0xa7cf, 0x0000, CanonicalizeUnique },
{ 0xa7d0, 0xa7d1, 0x0000, CanonicalizeAlternatingAligned },
{ 0xa7d2, 0xa7d5, 0x0000, CanonicalizeUnique },
{ 0xa7d6, 0xa7d9, 0x0000, CanonicalizeAlternatingAligned },
{ 0xa7da, 0xa7f4, 0x0000, CanonicalizeUnique },
{ 0xa7cb, 0xa7cb, 0xa567, CanonicalizeRangeHi },
{ 0xa7cc, 0xa7db, 0x0000, CanonicalizeAlternatingAligned },
{ 0xa7dc, 0xa7dc, 0xa641, CanonicalizeRangeHi },
{ 0xa7dd, 0xa7f4, 0x0000, CanonicalizeUnique },
{ 0xa7f5, 0xa7f6, 0x0000, CanonicalizeAlternatingUnaligned },
{ 0xa7f7, 0xab52, 0x0000, CanonicalizeUnique },
{ 0xab53, 0xab53, 0x03a0, CanonicalizeRangeHi },
Expand Down
Loading