From 01195ffb320eeb05c78738c8d91392080ee81633 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 25 Sep 2026 06:23:11 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20match/search=20=E8=B5=B0=20GetMethod=20?= =?UTF-8?q?=E6=B4=BE=E5=8F=91=EF=BC=8Cflags=20=E5=BD=92=E5=BA=8F=E4=B8=8E?= =?UTF-8?q?=20normalize=20=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/regexp.rs | 18 ++- crates/oxide_builtins/src/string/regex.rs | 153 ++++++++++++++---- .../oxide_vm/tests/builtins_regexp_tests.rs | 4 +- .../oxide_vm/tests/builtins_string_tests.rs | 22 +-- 4 files changed, 147 insertions(+), 50 deletions(-) diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 47b1e9e4..0dc16ac6 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -380,6 +380,18 @@ fn set_prop_by_si(obj: &mut JsObject, prop_name_si: u32, val: JsValue obj.ensure_hash_props().push(val); } +/// 规范化 flags 顺序为规范序(d g i m s u v y):同一组 flag 不论书写顺序 +/// 恒得同一串,保证 `flags`/`toString` 输出与 V8 一致。 +fn normalize_flags(flags: &str) -> String { + let mut out = String::new(); + for c in ['d', 'g', 'i', 'm', 's', 'u', 'v', 'y'] { + if flags.contains(c) { + out.push(c); + } + } + out +} + /// `RegExp(pattern, flags)` 构造逻辑:用 regress 引擎编译模式(ECMAScript 语法, /// 支持 backreference/lookaround/命名组/v-flag);非法模式抛 SyntaxError。 /// 编译结果存于对象的 native_fn 槽。 @@ -436,7 +448,8 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { (Vec::::new(), Vec::::new()) }; let pattern = String::from_utf16_lossy(&pattern_units); - let flags = String::from_utf16_lossy(&flags_units); + let flags = normalize_flags(&String::from_utf16_lossy(&flags_units)); + let flags_units = flags.encode_utf16().collect::>(); // u 与 v 互斥(regress 不校验此约束):编译前置抛 SyntaxError。 if flags.contains('u') && flags.contains('v') { @@ -1528,7 +1541,8 @@ pub fn regexp_symbol_search(vm: &mut H, args: &[u8]) -> NativeResult Ok(n) => n, Err(_) => return NativeResult::Err(crate::iterator::engine_error(vm, "cannot convert index")), }; - NativeResult::Ok(JsValue::float(n)) + // 匹配位置恒为非负整数,与无匹配臂的 -1(int)同口径。 + NativeResult::Ok(JsValue::int(n as i32)) } /// SameValue(val, +0):严格口径,-0 与 +0 不同(规范步骤用 SameValue 非 diff --git a/crates/oxide_builtins/src/string/regex.rs b/crates/oxide_builtins/src/string/regex.rs index a23d80a0..d8e0ce02 100644 --- a/crates/oxide_builtins/src/string/regex.rs +++ b/crates/oxide_builtins/src/string/regex.rs @@ -524,10 +524,27 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { Err(e) => NativeResult::Err(crate::iterator::engine_error(vm, &e)), }; } - // 非正则模式:Construct %RegExp%(undefined → 空模式、其余经 ToString), - // 再 GetMethod(rx, @@match) + Invoke;转换抛错(含 Symbol)原样传播。 + // string 值物化一次(text 借用至此结束),供下方两臂共用。 let s_units = text.units().into_owned(); let s_val = vm.new_string_units_owned(s_units); + // 对象模式:GetMethod(pattern, @@match) 先于构造(规范序);matcher 非 + // undefined 则 Call(matcher, pattern, «string») 原值返回(自定义对象与 + // 自置 @@match 的载体均走此臂)。 + if pattern_val.is_object() { + let matcher = match rx_get_match_method_opt(vm, pattern_val.as_js_object_ptr(), pattern_val) { + Ok(m) => m, + Err(e) => return NativeResult::Err(e), + }; + if let Some(matcher) = matcher { + return match vm.call_function_sync(matcher, pattern_val, &[s_val]) { + Ok(r) => NativeResult::Ok(r), + Err(e) => NativeResult::Err(crate::iterator::engine_error(vm, &e)), + }; + } + } + // 非对象 / matcher undefined:Construct %RegExp%(undefined → 空模式、其余 + // 经 ToString),再 GetMethod(rx, @@match) + Invoke;转换抛错(含 Symbol) + // 原样传播。 let regexp_ctor = vm.session().builtin_world().regexp_constructor.as_ptr() as *mut JsObject; let rx_val = match vm.construct_ctor(JsValue::from_js_object(regexp_ctor), &[pattern_val]) { Ok(v) => v, @@ -563,46 +580,104 @@ fn rx_get_match_method(vm: &mut H, rx: *mut JsObject, this_val: JsVal Ok(matcher) } +/// GetMethod(rx, @@match) 的 Option 形态:undefined/null 返回 None(落构造 +/// 路径),不可调用抛 TypeError,属性读抛错恢复原异常值。 +fn rx_get_match_method_opt( + vm: &mut H, rx: *mut JsObject, this_val: JsValue, +) -> Result, JsValue> { + let match_key = oxide_types::private_key::make_well_known_symbol_key(1); + let func = match vm.ordinary_get(unsafe { &*rx }, match_key, this_val) { + Ok(v) => v, + Err(e) => return Err(crate::iterator::engine_error(vm, &e)), + }; + if func.is_undefined() || func.is_null() { + return Ok(None); + } + if !crate::iterator::is_callable(func) { + return Err(crate::error::create_type_error(vm, "Symbol.match is not callable")); + } + Ok(Some(func)) +} + +/// GetMethod(rx, @@search):undefined/null 返回 None(落构造路径),不可调用 +/// 抛 TypeError,属性读抛错恢复原异常值。 +fn rx_get_search_method( + vm: &mut H, rx: *mut JsObject, this_val: JsValue, +) -> Result, JsValue> { + let search_key = oxide_types::private_key::make_well_known_symbol_key(3); + let func = match vm.ordinary_get(unsafe { &*rx }, search_key, this_val) { + Ok(v) => v, + Err(e) => return Err(crate::iterator::engine_error(vm, &e)), + }; + if func.is_undefined() || func.is_null() { + return Ok(None); + } + if !crate::iterator::is_callable(func) { + return Err(crate::error::create_type_error(vm, "Symbol.search is not callable")); + } + Ok(Some(func)) +} + /// `String.prototype.search(pattern)`:返回首个匹配位置(码元),无匹配返回 -1。 +/// +/// # 步骤 +/// 1. string = ToString(this)(转换异常传播)。 +/// 2. pattern 为对象时:GetMethod(pattern, @@search),searcher 非 undefined 则 +/// Call(searcher, pattern, «string») 原值返回(真 RegExp 默认 @@search 与 +/// 自定义对象均走此臂)。 +/// 3. Construct %RegExp%(null/undefined 映射对应文本,其余经 ToString)→ +/// GetMethod(rx, @@search) + Invoke(rx, «string»)。 +/// +/// # 边界与前提 +/// - 无参 / null 模式走构造路径:空模式命中串头返回 0。 +/// - 构造路径不预转换 pattern:构造器内部完成 ToString(异常原样传播)。 pub fn string_search(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.search called with {} args", args.len()); - // 参数读取守卫先行:无参调用缺省 searchString 为 undefined(args 仅含 this 槽)。 let pattern_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; - - // 正则判定与参数转换先行(&mut 路径),后借 this 扫描。 - let is_re = args.len() >= 2 && is_regexp_obj(pattern_val, vm); - let pattern: Vec = if args.len() >= 2 && !is_re { - try_string!(as_units(vm, pattern_val)).into_owned() - } else { - Vec::new() - }; let text = try_string!(this_text(vm, args)); - if args.len() < 2 { - return NativeResult::Ok(JsValue::int(-1)); - } - if is_re { - let re_ptr = pattern_val.as_js_object_ptr(); - let re = unsafe { &*re_ptr }; - let fn_ptr = match re.native_fn() { - Some(p) => p, - None => return NativeResult::Ok(JsValue::int(-1)), + // string 值物化(text 借用结束),供下方两臂共用。 + let s_units = text.units().into_owned(); + let s_val = vm.new_string_units_owned(s_units); + + // 对象模式:GetMethod(pattern, @@search) 先于构造(规范序);searcher 非 + // undefined 则 Call(searcher, pattern, «string») 原值返回。 + if pattern_val.is_object() { + let searcher = match rx_get_search_method(vm, pattern_val.as_js_object_ptr(), pattern_val) { + Ok(m) => m, + Err(e) => return NativeResult::Err(e), }; - // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - if let Some(m) = text.find_from_units(regex, 0) { - return NativeResult::Ok(JsValue::int(text.unit_pos(m.range().start) as i32)); + if let Some(searcher) = searcher { + return match vm.call_function_sync(searcher, pattern_val, &[s_val]) { + Ok(r) => NativeResult::Ok(r), + Err(e) => NativeResult::Err(crate::iterator::engine_error(vm, &e)), + }; } - return NativeResult::Ok(JsValue::int(-1)); } - // 空模式命中串头(规格口径)。 - if pattern.is_empty() { - return NativeResult::Ok(JsValue::int(0)); + + // 非对象 / searcher undefined:Construct %RegExp% + GetMethod(rx, @@search) + // + Invoke(rx, «string»);转换抛错(含 Symbol)原样传播。 + let regexp_ctor = vm.session().builtin_world().regexp_constructor.as_ptr() as *mut JsObject; + let rx_val = match vm.construct_ctor(JsValue::from_js_object(regexp_ctor), &[pattern_val]) { + Ok(v) => v, + Err(e) => return NativeResult::Err(e), + }; + if !rx_val.is_object() { + return NativeResult::Err(crate::error::create_type_error( + vm, + "search pattern constructor must return an object", + )); } - let s = text.units(); - if let Some(pos) = find_units(&s, &pattern, 0) { - return NativeResult::Ok(JsValue::int(pos as i32)); + let searcher = match rx_get_search_method(vm, rx_val.as_js_object_ptr(), rx_val) { + Ok(m) => m, + Err(e) => return NativeResult::Err(e), + }; + match searcher { + Some(searcher) => match vm.call_function_sync(searcher, rx_val, &[s_val]) { + Ok(r) => NativeResult::Ok(r), + Err(e) => NativeResult::Err(crate::iterator::engine_error(vm, &e)), + }, + None => NativeResult::Err(crate::error::create_type_error(vm, "RegExp @@search is not callable")), } - NativeResult::Ok(JsValue::int(-1)) } /// `String.prototype.isWellFormed()`:字符串无孤立 surrogate(每个码元要么是 @@ -659,14 +734,22 @@ pub fn string_to_well_formed(vm: &mut H, args: &[u8]) -> NativeResult pub fn string_normalize(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.normalize called with {} args", args.len()); use unicode_normalization::UnicodeNormalization; - let form = if args.len() > 1 { - try_string!(as_units(vm, vm.reg(args[1]))).into_owned() - } else { + // form 缺省或 undefined 按 "NFC" 处理(规范第 3 步),其余经 ToString。 + let form_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let form = if form_val.is_undefined() { "NFC".encode_utf16().collect() + } else { + try_string!(as_units(vm, form_val)).into_owned() }; // 规范化形式名按良形文本比对(NFC 等名无孤立 surrogate)。 let form_name = String::from_utf16_lossy(&form); + // this 转换先于 form 校验(规范第 2 步先于第 4 步)。 let s = try_string!(this_units(vm, args)); + // form 非四合法值抛 RangeError。 + match form_name.as_str() { + "NFC" | "NFD" | "NFKC" | "NFKD" => {} + _ => return NativeResult::Err(crate::error::create_range_error(vm, "normalize form is not valid")), + } let out = map_well_formed_segments(&s, |seg| match form_name.as_str() { "NFD" => seg.nfd().collect(), "NFKC" => seg.nfkc().collect(), diff --git a/crates/oxide_vm/tests/builtins_regexp_tests.rs b/crates/oxide_vm/tests/builtins_regexp_tests.rs index de068610..18289197 100644 --- a/crates/oxide_vm/tests/builtins_regexp_tests.rs +++ b/crates/oxide_vm/tests/builtins_regexp_tests.rs @@ -702,8 +702,8 @@ fn symbol_entries_accept_plain_object_this() { "(() => { var o = { exec: function() { return { index: 2 }; } }; return RegExp.prototype[Symbol.search].call(o, 'abcd'); })()", ) .unwrap(); - // ToNumber 结果为浮点形态(2.0)。 - assert_eq!(result.as_double(), 2.0); + // 匹配位置为整数形态(与无匹配臂的 -1 同口径)。 + assert_eq!(result.as_int(), 2); } #[test] diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 595fa824..fd995473 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -202,19 +202,19 @@ fn string_search_not_found() { } #[test] -fn string_search_no_args_returns_minus_one() { - // 无参调用缺省 searchString:args 仅含 this 槽,守卫返回 -1,不越界 panic。 +fn string_search_no_args_returns_zero() { + // 无参调用缺省 searchString 为 undefined:空模式命中串头返回 0。 let mut vm = Vm::new(); let result = eval(&mut vm, "'abc'.search()").unwrap(); - assert_eq!(result.as_int(), -1); + assert_eq!(result.as_int(), 0); } #[test] -fn string_search_undefined_arg_returns_minus_one() { - // 显式 undefined 参数:ToString(undefined)="undefined",在 "abc" 中无匹配返回 -1。 +fn string_search_undefined_arg_returns_zero() { + // 显式 undefined 参数:空模式命中串头返回 0。 let mut vm = Vm::new(); let result = eval(&mut vm, "'abc'.search(undefined)").unwrap(); - assert_eq!(result.as_int(), -1); + assert_eq!(result.as_int(), 0); } #[test] @@ -227,13 +227,13 @@ fn string_search_empty_pattern_returns_zero() { #[test] fn string_no_arg_guard_family_consistent() { - // 缺参时 searchString 按 ToString(undefined)="undefined" 参与查找——'abc' - // 不含 "undefined",indexOf 得 -1、includes/startsWith/endsWith 得 false - // ("undefined" 命中的判别断言见 string_missing_arg_searches_undefined); - // search 缺参仍走旧守卫返回 -1。 + // 缺参时 search 按空模式命中串头得 0;indexOf 按 ToString(undefined)= + // "undefined" 参与查找——'abc' 不含 "undefined" 得 -1、includes/startsWith/ + // endsWith 得 false("undefined" 命中的判别断言见 + // string_missing_arg_searches_undefined)。 let mut vm = Vm::new(); let result = eval(&mut vm, "'abc'.search()").unwrap(); - assert_eq!(result.as_int(), -1); + assert_eq!(result.as_int(), 0); let result = eval(&mut vm, "'abc'.indexOf()").unwrap(); assert_eq!(result.as_int(), -1); let result = eval(&mut vm, "'abc'.includes()").unwrap();