diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 29998e4..5787504 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -146,6 +146,26 @@ pub fn drop_regexp_native(obj: &mut JsObject) -> u64 { std::mem::size_of::() as u64 } +/// 深拷贝已编译正则到新对象(GC 搬移 / 晋升流程用)。 +/// +/// 与 drop 配对:新对象获得独立的 `Box`,源对象保留自己的指针, +/// 两侧各自释放恰好一次,杜绝跨 arena 克隆后的指针别名双释放。 +pub fn clone_regexp_native(old_obj: &JsObject, new_obj: &mut JsObject) { + if !old_obj.is_regexp_obj() { + return; + } + let Some(ptr) = old_obj.native_fn() else { + return; + }; + let regex_ptr = ptr.as_ptr() as *const regress::Regex; + if regex_ptr.is_null() { + return; + } + // SAFETY: old_obj 的 native_fn 槽存 `Box` 指针,对象存活期间有效。 + let cloned_ptr = Box::into_raw(Box::new(unsafe { (&*regex_ptr).clone() })); + new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned_ptr as *const ()) })); +} + /// `RegExp.prototype.test(string)`:判断是否匹配。global 模式下从 lastIndex 开始匹配。 pub fn regexp_test(vm: &mut H, args: &[u8]) -> NativeResult { let re_ptr = match get_regexp_ptr(vm, args) { diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index c641487..9cfdfca 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -1613,7 +1613,18 @@ pub fn string_match_all_next(vm: &mut H, args: &[u8]) -> NativeResult None => parts.push(String::new()), } } - idx = range.end; + // 空匹配(range.start == range.end)必须推进至少一个 UTF-8 字符,否则同一 + // 位置反复空匹配死循环;空匹配落在串尾时把游标推到串尾之后,保证下一轮 + // 直接耗尽(与 regress find_iter 的 next_right_pos 推进语义一致)。 + idx = if range.end > range.start { + range.end + } else { + range.end + + input_str + .get(range.end..) + .and_then(|s| s.chars().next()) + .map_or(1, char::len_utf8) + }; vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32)); let arr_val = make_string_array(vm, parts); make_match_done_result(vm, arr_val) diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 7075eea..ab7d116 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -6,7 +6,7 @@ use oxide_types::value::JsValue; use rustc_hash::FxBuildHasher; use crate::vm::Vm; -use oxide_builtins::{array_buffer, data_view, map, set, typed_array}; +use oxide_builtins::{array_buffer, data_view, map, regexp, set, typed_array}; impl Vm { pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool { @@ -88,6 +88,9 @@ impl Vm { crate::async_generator::clone_async_generator_native_with_rewrite(src_ref, dst_ref, |value| { self.promote_value_if_epoch_object(value, forwarding) }); + } else if src_ref.is_regexp_obj() { + // 已编译正则是 Box 深拷贝到新对象:源盒随 epoch 释放,互不共享。 + regexp::clone_regexp_native(src_ref, dst_ref); } dst } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 7e985a8..f989313 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -436,6 +436,9 @@ impl SessionGc { crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value); } else if old_ref.is_generator_obj() { crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value); + } else if old_ref.is_regexp_obj() { + // 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。 + regexp::clone_regexp_native(old_ref, new_ref); } forwarding.insert(old_ptr, new_ptr); freed_bytes += Self::drop_session_object_heap_data(old_ptr); diff --git a/crates/oxide_vm/tests/builtins_regexp_tests.rs b/crates/oxide_vm/tests/builtins_regexp_tests.rs index bd13b85..9cca4dd 100644 --- a/crates/oxide_vm/tests/builtins_regexp_tests.rs +++ b/crates/oxide_vm/tests/builtins_regexp_tests.rs @@ -13,6 +13,23 @@ fn to_str(vm: &Vm, val: JsValue) -> String { vm.lookup_str(val).unwrap_or_default() } +#[test] +fn regexp_survives_gc_and_still_matches() { + // 回归:正则存入全局对象触发晋升/GC 搬移后,native_fn 槽的已编译 Box + // 必须深拷贝到新对象(而非共享指针),否则 epoch 释放与 teardown 双重释放。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "var r = /ab+c/g; globalThis.r = r; r.test('xxabbcx')").unwrap(); + assert!(result.as_bool()); + + // 触发完整收集:存活正则克隆进新 arena,旧 Box 由 sweep 释放。 + vm.reset(); + + let result = eval(&mut vm, "globalThis.r.test('xxabbcx')").unwrap(); + assert!(result.as_bool(), "GC 后正则应仍可匹配"); + let result = eval(&mut vm, "globalThis.r.exec('xabbbcx')[0]").unwrap(); + assert_eq!(to_str(&vm, result), "abbbc"); +} + // --- RegExp constructor --- #[test] diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 951dfdd..71c93bd 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -696,6 +696,25 @@ fn string_match_all_next_exhausted() { assert!(done); } +#[test] +fn string_match_all_empty_match_advances() { + // 回归:空匹配(range.start == range.end)后游标不前进会在同位置反复 + // 产出空匹配死循环;每位置应恰好一个空匹配后在有限步内耗尽。 + let mut vm = Vm::new(); + let result = + eval(&mut vm, "var it = 'abc'.matchAll(/(?:)/g); var c = 0; while (!it.next().done) { c++; } c").unwrap(); + assert_num_eq(result, 4.0); +} + +#[test] +fn string_match_all_star_empty_advances() { + // /a*/g 在 'baaab' 上:空匹配(首尾)+ 长匹配混合,须在有限步内耗尽。 + let mut vm = Vm::new(); + let result = + eval(&mut vm, "var it = 'baaab'.matchAll(/a*/g); var c = 0; while (!it.next().done) { c++; } c").unwrap(); + assert_num_eq(result, 4.0); +} + // ── substring 测试 ── #[test]