A third, independent root cause in the family of #9429 / #9430, found while fixing those two but deliberately left out of PR #9437.
When a split pattern can only be compiled by fancy-regex (look-around, backreferences), perry does not run RegExp.prototype [ @@split ] at all. The fallback walks find_iter and slices between matches, which differs from the spec in two ways:
"a,b,".split(/(?<=,)/) // node ["a,","b,"] perry ["a,","b,",""]
"aXbXc".split(/((?<=a)X)/) // node ["a","X","bXc"] perry ["a","bXc"]
- A trailing
"" the spec never produces. @@split's loop is bounded by q < size, so a match ending at the end of the subject cannot open a final empty chunk. Slicing between matches emits one.
- Captured groups are not spliced into the result.
@@split interleaves every capture of the separator match; the fallback discards them.
#9427 widened the reach of this
#9427 fixes multiline anchors by rewriting ^ into (?:\A|(?<=[\n\r
])) and $ into (?:\z|(?=[…])). That rewrite makes the pattern lookaround-bearing, which moves it onto the fancy-regex lane — so patterns that previously took the correct path now take this one:
"a\r\nb".split(/^/gm) // gains a spurious LEADING "" after #9427
Both directions measured on a tree with #9427 merged. This is not an argument against #9427 — its own fixture is byte-identical to node — but it means the gap now reaches ordinary /m code rather than only explicit look-around.
A runtime test currently pins the wrong answer
fancy_lookbehind_split in the regex tests asserts the fallback's output, so it will fail when this is fixed. That assertion needs to be replaced with node's, not preserved.
Fix shape
Route the fancy lane through the same @@split algorithm the other lanes use: honour the q < size bound, and splice 1 .. captures.len() into the output after each separator match. The positional search added in PR #9437 (captures_from_pos on the full haystack) is the primitive this needs — a sliced search cannot implement @@split's q/p cursors correctly for look-around patterns, which is why the two changes are ordered this way.
Verification bar
A gap fixture demonstrated failing against a compiler built from unfixed origin/main, byte-compared to node --experimental-strip-types, covering: lookbehind and lookahead separators, with and without capture groups, matches at the start and end of the subject, an empty subject, a limit argument, and the /^/gm and /$/gm shapes that #9427 rewrites onto this lane.
Found by the differential stress-test of claude-code under perry.
A third, independent root cause in the family of #9429 / #9430, found while fixing those two but deliberately left out of PR #9437.
When a
splitpattern can only be compiled by fancy-regex (look-around, backreferences), perry does not runRegExp.prototype [ @@split ]at all. The fallback walksfind_iterand slices between matches, which differs from the spec in two ways:""the spec never produces.@@split's loop is bounded byq < size, so a match ending at the end of the subject cannot open a final empty chunk. Slicing between matches emits one.@@splitinterleaves every capture of the separator match; the fallback discards them.#9427 widened the reach of this
#9427 fixes multiline anchors by rewriting
^into(?:\A|(?<=[\n\r ]))and$into(?:\z|(?=[…])). That rewrite makes the pattern lookaround-bearing, which moves it onto the fancy-regex lane — so patterns that previously took the correct path now take this one:Both directions measured on a tree with #9427 merged. This is not an argument against #9427 — its own fixture is byte-identical to node — but it means the gap now reaches ordinary
/mcode rather than only explicit look-around.A runtime test currently pins the wrong answer
fancy_lookbehind_splitin the regex tests asserts the fallback's output, so it will fail when this is fixed. That assertion needs to be replaced with node's, not preserved.Fix shape
Route the fancy lane through the same
@@splitalgorithm the other lanes use: honour theq < sizebound, and splice1 .. captures.len()into the output after each separator match. The positional search added in PR #9437 (captures_from_poson the full haystack) is the primitive this needs — a sliced search cannot implement@@split'sq/pcursors correctly for look-around patterns, which is why the two changes are ordered this way.Verification bar
A gap fixture demonstrated failing against a compiler built from unfixed
origin/main, byte-compared tonode --experimental-strip-types, covering: lookbehind and lookahead separators, with and without capture groups, matches at the start and end of the subject, an empty subject, alimitargument, and the/^/gmand/$/gmshapes that #9427 rewrites onto this lane.Found by the differential stress-test of claude-code under perry.