diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 75592016..4388ee03 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -1606,22 +1606,12 @@ pub fn regexp_symbol_split(vm: &mut H, args: &[u8]) -> NativeResult { let units = haystack.as_match_text().units().to_vec(); let size = units.len(); - // lim:缺省 2^32-1,否则 ToUint32(完整 ToNumber 传播转换异常; - // NaN/±0 归零、±∞ 归 2^32-1、负数回绕)。 - let lim = if args.len() > 2 { - let l = match oxide_runtime_api::to_number_full(vm.reg(args[2]), vm) { - Ok(n) => n, - Err(e) => return NativeResult::Err(crate::iterator::engine_error(vm, &e)), - }; - if l.is_nan() || l == 0.0 { - 0usize - } else if l.is_infinite() { - u32::MAX as usize - } else { - l.trunc().rem_euclid(4_294_967_296.0) as usize - } - } else { - u32::MAX as usize + // lim:缺省或 undefined 为 2^32-1,否则 ToUint32(完整 ToNumber 传播 + // 转换异常;NaN/±0/±∞ 归零、负数回绕)。 + let lim_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let lim = match crate::string::split_limit_to_uint32(vm, lim_val) { + Ok(n) => n, + Err(e) => return NativeResult::Err(e), }; if lim == 0 { return NativeResult::Ok(make_units_array(vm, Vec::new())); diff --git a/crates/oxide_builtins/src/string/common.rs b/crates/oxide_builtins/src/string/common.rs index 29317a72..f9c1dd12 100644 --- a/crates/oxide_builtins/src/string/common.rs +++ b/crates/oxide_builtins/src/string/common.rs @@ -350,6 +350,26 @@ pub(crate) fn as_units<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result(vm: &mut H, limit_val: JsValue) -> Result { + if limit_val.is_undefined() { + return Ok(u32::MAX as usize); + } + let n = match oxide_runtime_api::to_number_full(limit_val, vm) { + Ok(n) => n, + Err(e) => return Err(crate::iterator::engine_error(vm, &e)), + }; + if n.is_nan() || n == 0.0 || n.is_infinite() { + return Ok(0); + } + Ok(n.trunc().rem_euclid(4_294_967_296.0) as usize) +} + // ── 数组产出 ───────────────────────────────────────────────────────────── /// 以已构造的字符串值构建字符串数组(元素零拷贝落地),供逐单元产出路径 diff --git a/crates/oxide_builtins/src/string/regex.rs b/crates/oxide_builtins/src/string/regex.rs index 17f4865e..0dd328db 100644 --- a/crates/oxide_builtins/src/string/regex.rs +++ b/crates/oxide_builtins/src/string/regex.rs @@ -11,7 +11,7 @@ use crate::regexp::build_groups_object; use super::common::{try_string, MatchText}; use super::{ as_units, byte_to_unit, find_units, make_string_array_values, make_units_array, map_well_formed_segments, - this_text, this_units, unit_to_byte, + split_limit_to_uint32, this_text, this_units, unit_to_byte, }; // ── 正则替换(单元口径) ──────────────────────────────────────────────── @@ -162,90 +162,109 @@ fn string_arm_replace( // ── 拆分 / 正则匹配 ───────────────────────────────────────────────────── +/// GetMethod(sep, @@split):undefined/null 返回 None(落字符串臂),不可调用 +/// 抛 TypeError,属性读抛错恢复原异常值。 +fn rx_get_split_method(vm: &mut H, sep: JsValue) -> Result, JsValue> { + let split_key = oxide_types::private_key::make_well_known_symbol_key(4); + let ptr = sep.as_js_object_ptr(); + // SAFETY: 调用方已校验 sep 为非空对象值。 + let func = match vm.ordinary_get(unsafe { &*ptr }, split_key, sep) { + Ok(v) => v, + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + return Err(crate::error::create_type_error(vm, "cannot read @@split")); + } + }; + 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.split is not callable")); + } + Ok(Some(func)) +} + /// `String.prototype.split(separator, limit)`:按分隔符拆分为字符串数组; /// 分隔符可为 RegExp(含捕获组)或字符串。空分隔按单元逐个产出(规格口径, /// 孤立 surrogate 为 1 单元元素)。 +/// +/// # 步骤 +/// 1. receiver 前置校验(RequireObjectCoercible,纯 is_* 读取,先于一切分叉)。 +/// 2. separator 为对象时:GetMethod(separator, @@split),可调用则 +/// Call(splitter, separator, «thisValue, limit»)——传原始 this/limit +/// 寄存器不预转换,结果原值返回(RegExp 分隔经此委托 Symbol.split)。 +/// 3. string = ToString(this)(转换异常传播)。 +/// 4. lim:limit 缺省或 undefined 为 2^32-1,否则 ToUint32(mod 2^32 回绕)。 +/// 5. separatorString = ToString(separator)(转换异常传播;先于 lim = 0 判定)。 +/// 6. lim = 0 → 空数组(先于 sep-undefined 判定)。 +/// 7. separator 为 undefined → [string]。 +/// 8. 空分隔逐码元产出(clamp(lim, 0, 串长));非空分隔搜索循环,尾段恒推。 +/// +/// # 边界与前提 +/// - 规范序位:GetMethod 派发先于 ToString(this),ToUint32(limit) 先于 +/// ToString(separator),ToString(separator) 先于 lim = 0 判定, +/// lim = 0 判定先于 sep-undefined 判定。 +/// - 各转换(this/limit/separator)抛出的原生异常原样传播。 pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.split called with {} args", args.len()); - // 参数转换先行:separator 为对象时 ToString 可能触发用户代码(&mut 路径), - // limit 仅纯函数计算;之后借 this 源串扫描切分。 - let sep_val = if args.len() >= 2 { Some(vm.reg(args[1])) } else { None }; - let is_undefined_sep = sep_val.map(|v| v.is_undefined()).unwrap_or(true); - let is_re = match sep_val { - Some(v) if !v.is_undefined() => is_regexp_obj(v, vm), - _ => false, - }; - // 类正则对象是否持有编译正则:native_fn 存在才是真 RegExp 实例,命中正则切分路径; - // 无编译正则的类正则对象(如 Object.create(RegExp.prototype))回退字符串路径, - // 须按 ToString 文本切分,且转换(&mut 路径)须先于 this 借用完成。 - let has_native_re = is_re && { - let sep = sep_val.expect("separator present when is_re is true"); - let re_ptr = sep.as_js_object_ptr(); - // SAFETY: is_re 已保证 sep_val 为非空对象且 proto 恒等 RegExp.prototype。 - let re = unsafe { &*re_ptr }; - re.native_fn().is_some() - }; - // ToUint32(limit),缺省为 2^32-1。 - let limit = if args.len() > 2 { - let l = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])); - if l.is_infinite() { - u32::MAX as usize - } else { - (l.max(0.0).trunc() as u64).min(u32::MAX as u64) as usize + let this_val = vm.reg(args[0]); + + // receiver 前置校验:null/undefined 抛 TypeError(RequireObjectCoercible, + // 纯 is_* 读取无用户代码,先于一切分叉);symbol 的 ToString 必败,提前抛。 + if this_val.is_null() || this_val.is_undefined() { + return NativeResult::Err(crate::error::create_type_error( + vm, + "String.prototype method called on null or undefined", + )); + } + if this_val.is_symbol() { + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")); + } + + let sep_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let limit_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + + // 对象分隔符经 GetMethod 派发:调用定义则 Call 后原值返回,须先于 + // ToString(this) 短路(实参为原始寄存器,不预转换)。 + if sep_val.is_object() { + let splitter = try_string!(rx_get_split_method(vm, sep_val)); + if let Some(splitter) = splitter { + return match vm.call_function_sync(splitter, sep_val, &[this_val, limit_val]) { + Ok(r) => NativeResult::Ok(r), + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + NativeResult::Err(crate::error::create_type_error(vm, "split matcher call failed")) + } + }; } - } else { - u32::MAX as usize - }; - let sep_units: Vec = if is_undefined_sep || has_native_re { - Vec::new() - } else { - let sep = sep_val.expect("separator present when not undefined and not native regexp"); - try_string!(as_units(vm, sep)).into_owned() - }; + } + + // string = ToString(this)。 let s: Vec = try_string!(this_units(vm, args)).into_owned(); - // 规范:separator 为 undefined 时返回 [this]。 - if is_undefined_sep { + + // lim = ToUint32(limit):undefined 为 2^32-1,对象经完整 ToNumber(异常传播)。 + let lim = try_string!(split_limit_to_uint32(vm, limit_val)); + + // separatorString = ToString(separator)(先于 lim = 0 判定:sep 转换 + // 抛错时 lim = 0 的早退不生效)。 + let sep_units: Vec = try_string!(as_units(vm, sep_val)).into_owned(); + + if lim == 0 { + return NativeResult::Ok(make_units_array(vm, Vec::new())); + } + // separator 为 undefined 时返回单元素 [string]。 + if sep_val.is_undefined() { return NativeResult::Ok(make_units_array(vm, vec![s.to_vec()])); } - if is_re { - let re_ptr = sep_val.unwrap().as_js_object_ptr(); - let re = unsafe { &*re_ptr }; - if let Some(fn_ptr) = re.native_fn() { - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - // 片段物化字符串值,未匹配捕获组推 undefined(结果数组按值混排)。 - let mut parts: Vec = Vec::new(); - let mut last_end = 0; - for m in regex.find_from_utf16(&s, 0) { - if parts.len() >= limit { - break; - } - let range = m.range(); - parts.push(vm.new_string_units_owned(s[last_end..range.start].to_vec())); - if parts.len() >= limit { - break; - } - for i in 1..=m.captures.len() { - if parts.len() >= limit { - break; - } - match m.group(i) { - Some(g) => parts.push(vm.new_string_units_owned(s[g.start..g.end].to_vec())), - None => parts.push(JsValue::undefined()), - } - } - last_end = range.end; - } - if last_end <= s.len() && parts.len() < limit { - parts.push(vm.new_string_units(&s[last_end..])); - } - return NativeResult::Ok(make_string_array_values(vm, parts)); - } - // 无原生正则的类正则对象回退到字符串路径。 - } + if sep_units.is_empty() { // 每单元一个元素(ASCII 走单字符缓存零分配,其余 1 单元串创建)。 - let mut values = Vec::with_capacity(s.len().min(limit)); - for &u in s.iter().take(limit) { + let mut values = Vec::with_capacity(s.len().min(lim)); + for &u in s.iter().take(lim) { values.push(match vm.single_unit(u) { Some(v) => v, None => vm.new_string_units(&[u]), @@ -253,11 +272,12 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { } return NativeResult::Ok(make_string_array_values(vm, values)); } + // 非空字符串分隔:手动查找循环 + limit。 let mut parts: Vec> = Vec::new(); let mut start = 0; loop { - if parts.len() >= limit { + if parts.len() >= lim { break; } match find_units(&s, &sep_units, start) { diff --git a/crates/oxide_kernel/src/builtin/bind.rs b/crates/oxide_kernel/src/builtin/bind.rs index 1a25fc17..9132b794 100644 --- a/crates/oxide_kernel/src/builtin/bind.rs +++ b/crates/oxide_kernel/src/builtin/bind.rs @@ -256,7 +256,7 @@ impl BuiltinWorld { ("padEnd", methods.pad_end, 1), ("startsWith", methods.starts_with, 1), ("endsWith", methods.ends_with, 1), - ("split", methods.split, 1), + ("split", methods.split, 2), ("replace", methods.replace, 2), ("match", methods.match_fn, 1), ("search", methods.search, 1), diff --git a/crates/oxide_vm/tests/builtins_regexp_tests.rs b/crates/oxide_vm/tests/builtins_regexp_tests.rs index 8283ad65..5d45f542 100644 --- a/crates/oxide_vm/tests/builtins_regexp_tests.rs +++ b/crates/oxide_vm/tests/builtins_regexp_tests.rs @@ -789,6 +789,16 @@ fn symbol_split_limit_zero_and_to_uint32() { assert_eq!(to_str(&vm, result), "limit-boom"); } +#[test] +fn symbol_split_undefined_limit_full() { + // limit 缺省与显式 undefined 均为 2^32-1 全量(非 ToUint32(NaN)=0)。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "RegExp.prototype[Symbol.split].call(/,/, 'a,b', undefined).length").unwrap(); + assert!(result.as_int() == 2); + let result = eval(&mut vm, "'a,b'.split(/,/, undefined).length").unwrap(); + assert!(result.as_int() == 2); +} + #[test] fn symbol_match_all_species_matcher_and_cached_lastindex() { let mut vm = Vm::new(); diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 60e474f0..bd1b79f5 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -702,6 +702,132 @@ fn string_split_real_regex_still_regex_path() { assert_eq!(to_str(&vm, obj.get_prop_at(1)), ""); } +#[test] +fn string_split_regexp_zero_width_skips_separator() { + // 零宽匹配不推分隔段:"hello".split(new RegExp) 逐边界零宽,结果即逐码元。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.split(new RegExp).join('|')").unwrap(); + assert_eq!(to_str(&vm, result), "h|e|l|l|o"); +} + +#[test] +fn string_split_zero_width_stars_pattern() { + // /l*/ 每边界零宽匹配:仅非零宽命中处分段,结果 ["h","e","o"]。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.split(/l*/g).join('|')").unwrap(); + assert_eq!(to_str(&vm, result), "h|e|o"); +} + +#[test] +fn string_split_zero_width_trailing_excluded() { + // "x".split(/(?:)/) 尾段取自 lastMatchEnd,串尾零宽匹配被循环排除 → ["x"]; + // 非零宽末匹配尾段照推 → "x".split(/./) 得 ["",""]。 + let mut vm = Vm::new(); + let one = eval(&mut vm, "'x'.split(/(?:)/).join('|')").unwrap(); + assert_eq!(to_str(&vm, one), "x"); + let two = eval(&mut vm, "'x'.split(/./).join('|')").unwrap(); + assert_eq!(to_str(&vm, two), "|"); +} + +#[test] +fn string_split_empty_input_regexp() { + // 空串输入:可命中正则单次 exec 得 [],不可命中得 [""]。 + let mut vm = Vm::new(); + let hit = eval(&mut vm, "''.split(/(?:)/).length").unwrap(); + assert_eq!(hit.as_int(), 0); + let miss = eval(&mut vm, "''.split(/x/).length").unwrap(); + assert_eq!(miss.as_int(), 1); +} + +#[test] +fn string_split_limit_to_uint32_wrap() { + // limit 按 ToUint32 回绕:2^32 → 0 空数组、-1 → 2^32-1 全量、NaN → 0、 + // 缺省与 undefined 均全量。 + let mut vm = Vm::new(); + let wrap = eval(&mut vm, "'a,b,c'.split(',', 2 ** 32).length").unwrap(); + assert_eq!(wrap.as_int(), 0); + let neg = eval(&mut vm, "'a,b,c'.split(',', -1).length").unwrap(); + assert_eq!(neg.as_int(), 3); + let nan = eval(&mut vm, "'a,b,c'.split(',', NaN).length").unwrap(); + assert_eq!(nan.as_int(), 0); + let undef = eval(&mut vm, "'a,b,c'.split(',', undefined).length").unwrap(); + assert_eq!(undef.as_int(), 3); +} + +#[test] +fn string_split_limit_object_full_tonumber() { + // limit 对象经完整 ToNumber:valueOf 直取、转换异常传播。 + let mut vm = Vm::new(); + let via_value_of = eval(&mut vm, "'a|b|c'.split('|', { valueOf: function () { return 2; } }).length").unwrap(); + assert_eq!(via_value_of.as_int(), 2); + let err = eval( + &mut vm, + "'a|b'.split('|', { valueOf: function () { throw new RangeError('limit-boom'); } })", + ) + .unwrap_err(); + assert!(err.contains("RangeError"), "limit 对象转换异常应传播,实际: {err}"); +} + +#[test] +fn string_split_order_limit_before_separator_tostring() { + // 序位:ToUint32(limit) 先于 ToString(separator)——limit 的 valueOf 抛错胜出。 + let mut vm = Vm::new(); + let err = eval( + &mut vm, + concat!( + "'foo'.split(", + "{ toString: function () { throw new RangeError('sep-boom'); } }, ", + "{ valueOf: function () { throw new RangeError('limit-boom'); } })" + ), + ) + .unwrap_err(); + assert!(err.contains("limit-boom"), "limit 转换应先于 separator 转换,实际: {err}"); +} + +#[test] +fn string_split_getmethod_dispatch() { + // 对象分隔符经 GetMethod 派发:Call(splitter, sep, «this, limit»)传原始 + // 寄存器(this 未 ToString、limit 未 ToUint32),结果原值返回。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + concat!( + "var sep = {}; sep[Symbol.split] = function (s, l) { return [s, l]; }; ", + "var o = Object.create(String.prototype); ", + "var r = o.split(sep, 7); ", + "(r[0] === o) + '|' + r[1]" + ), + ) + .unwrap(); + assert_eq!(to_str(&vm, result), "true|7"); +} + +#[test] +fn string_split_getmethod_short_circuits_this_tostring() { + // GetMethod 派发先于 ToString(this):this 的 toString 抛错不被触发。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + concat!( + "var o = new String('x'); ", + "o.toString = function () { throw new RangeError('this-boom'); }; ", + "var sep = {}; sep[Symbol.split] = function () { return 42; }; ", + "o.split(sep)" + ), + ) + .unwrap(); + assert_eq!(result.as_int(), 42); +} + +#[test] +fn string_split_zero_width_missing_capture_not_pushed() { + // 零宽匹配不推分隔段与捕获组:串内仅零宽命中时缺席捕获组不占位, + // 结果只剩尾段。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "'a'.split(/(b)*/).join('|')").unwrap(); + assert_eq!(to_str(&vm, result), "a"); +} + // ── match 测试 ── #[test]