LibRegex: Respect V8 astral literal lastIndex behavior
Preserve V8's behavior for bare single-astral literals when a unicode
global search starts in the middle of a surrogate pair. We were
snapping that lastIndex back to the pair start unconditionally,
which let /😀/gu and /\u{1F600}/gu match where V8 returns null.
Expose that literal shape from LibRegex to LibJS and add runtime
coverage for the bare literal case alongside a grouped control.
This commit is contained in:
parent
29c2fb9574
commit
f627b7dcbb
8 changed files with 52 additions and 1 deletions
|
|
@ -168,7 +168,8 @@ static ThrowCompletionOr<Value> regexp_builtin_exec(VM& vm, RegExpObject& regexp
|
|||
|| has_flag(flag_bits, RegExpObject::Flags::UnicodeSets);
|
||||
if (unicode_mode && last_index > 0 && last_index < utf16_view.length_in_code_units()) {
|
||||
if (utf16_view.code_unit_at(last_index) >= 0xDC00 && utf16_view.code_unit_at(last_index) <= 0xDFFF
|
||||
&& utf16_view.code_unit_at(last_index - 1) >= 0xD800 && utf16_view.code_unit_at(last_index - 1) <= 0xDBFF) {
|
||||
&& utf16_view.code_unit_at(last_index - 1) >= 0xD800 && utf16_view.code_unit_at(last_index - 1) <= 0xDBFF
|
||||
&& !compiled_regex->is_single_non_bmp_literal()) {
|
||||
--last_index;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ unsigned int ECMAScriptRegex::total_groups() const
|
|||
return m_impl->rust_regex.total_groups();
|
||||
}
|
||||
|
||||
bool ECMAScriptRegex::is_single_non_bmp_literal() const
|
||||
{
|
||||
return m_impl->rust_regex.is_single_non_bmp_literal();
|
||||
}
|
||||
|
||||
Vector<ECMAScriptNamedCaptureGroup> const& ECMAScriptRegex::named_groups() const
|
||||
{
|
||||
return m_impl->named_groups;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ public:
|
|||
/// Total number of capture groups including group 0.
|
||||
unsigned int total_groups() const;
|
||||
|
||||
bool is_single_non_bmp_literal() const;
|
||||
|
||||
/// Named capture groups with their indices.
|
||||
Vector<ECMAScriptNamedCaptureGroup> const& named_groups() const;
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,20 @@ pub unsafe extern "C" fn rust_regex_capture_count(regex: *const RustRegex) -> u3
|
|||
regex.0.capture_count()
|
||||
}
|
||||
|
||||
/// Return whether this regex is a whole-pattern literal for a single non-BMP
|
||||
/// code point in unicode mode.
|
||||
///
|
||||
/// # Safety
|
||||
/// `regex` must be a valid pointer from `rust_regex_compile`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_is_single_non_bmp_literal(regex: *const RustRegex) -> bool {
|
||||
if regex.is_null() {
|
||||
return false;
|
||||
}
|
||||
let regex = unsafe { &*regex };
|
||||
regex.0.is_single_non_bmp_literal()
|
||||
}
|
||||
|
||||
/// Find all non-overlapping matches and return a flat array of (start, end) i32 pairs.
|
||||
/// Returns the number of matches (NOT the number of i32s). The output buffer must have
|
||||
/// space for at least `max_matches * 2` i32s. Returns -1 if the buffer is too small.
|
||||
|
|
|
|||
|
|
@ -332,6 +332,15 @@ impl Regex {
|
|||
&self.program.named_groups
|
||||
}
|
||||
|
||||
pub fn is_single_non_bmp_literal(&self) -> bool {
|
||||
(self.flags.unicode || self.flags.unicode_sets)
|
||||
&& matches!(
|
||||
self.literal_u16.as_deref(),
|
||||
Some([high, low])
|
||||
if (0xD800..=0xDBFF).contains(high) && (0xDC00..=0xDFFF).contains(low)
|
||||
)
|
||||
}
|
||||
|
||||
/// Find all non-overlapping matches starting from `start`.
|
||||
/// Writes (match_start, match_end) i32 pairs directly into `result_buf`.
|
||||
/// Returns number of matches found, or -1 if buffer is too small.
|
||||
|
|
|
|||
|
|
@ -499,6 +499,11 @@ unsigned int CompiledRustRegex::total_groups() const
|
|||
return m_capture_count;
|
||||
}
|
||||
|
||||
bool CompiledRustRegex::is_single_non_bmp_literal() const
|
||||
{
|
||||
return rust_regex_is_single_non_bmp_literal(m_regex);
|
||||
}
|
||||
|
||||
int CompiledRustRegex::test(Utf16View input, size_t start_pos) const
|
||||
{
|
||||
if (input.has_ascii_storage()) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public:
|
|||
unsigned int capture_count() const;
|
||||
/// Total number of capture groups including group 0.
|
||||
unsigned int total_groups() const;
|
||||
bool is_single_non_bmp_literal() const;
|
||||
|
||||
/// Find all non-overlapping matches. Returns number of matches found.
|
||||
/// Results are written as (start, end) i32 pairs to the internal find_all buffer.
|
||||
|
|
|
|||
14
Tests/LibJS/Runtime/regexp-bare-astral-literal-lastindex.js
Normal file
14
Tests/LibJS/Runtime/regexp-bare-astral-literal-lastindex.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
test("bare astral literals do not snap a global unicode search back to the pair start", () => {
|
||||
let matcher = /😀/gu;
|
||||
matcher.lastIndex = 1;
|
||||
expect(matcher.exec("😀")).toBeNull();
|
||||
expect(matcher.lastIndex).toBe(0);
|
||||
|
||||
matcher = /(?:😀)/gu;
|
||||
matcher.lastIndex = 1;
|
||||
let match = matcher.exec("😀");
|
||||
expect(match).not.toBeNull();
|
||||
expect(match[0]).toBe("😀");
|
||||
expect(match.index).toBe(0);
|
||||
expect(matcher.lastIndex).toBe(2);
|
||||
});
|
||||
Loading…
Reference in a new issue