fix: split 委托 Symbol.split 并补零宽与 limit 口径

This commit is contained in:
2026-09-22 07:45:34 +00:00
parent 169f4b2984
commit fb9365bde2
6 changed files with 258 additions and 92 deletions
+6 -16
View File
@@ -1606,22 +1606,12 @@ pub fn regexp_symbol_split<H: VmHost>(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()));
@@ -350,6 +350,26 @@ pub(crate) fn as_units<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result<Cow
}
}
// ── split limit 换算 ────────────────────────────────────────────────────
/// split 的 limit 参数 → ToUint32 口径的 usize:undefined 为 2^32-1(全量);
/// 其余值经完整 ToNumber(对象走 ToPrimitive number hint,Symbol 抛 TypeError),
/// NaN/±0/±∞ 归 0,有限值按 mod 2^32 回绕(负数回绕,-1 为 2^32-1)。
/// 对象转换抛出的原生异常原样传播。
pub(crate) fn split_limit_to_uint32<H: VmHost>(vm: &mut H, limit_val: JsValue) -> Result<usize, JsValue> {
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)
}
// ── 数组产出 ─────────────────────────────────────────────────────────────
/// 以已构造的字符串值构建字符串数组(元素零拷贝落地),供逐单元产出路径
+95 -75
View File
@@ -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<H: VmHost>(
// ── 拆分 / 正则匹配 ─────────────────────────────────────────────────────
/// GetMethod(sep, @@split):undefined/null 返回 None(落字符串臂),不可调用
/// 抛 TypeError,属性读抛错恢复原异常值。
fn rx_get_split_method<H: VmHost>(vm: &mut H, sep: JsValue) -> Result<Option<JsValue>, 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<H: VmHost>(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<u16> = 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<u16> = 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<u16> = 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<JsValue> = 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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
return NativeResult::Ok(make_string_array_values(vm, values));
}
// 非空字符串分隔:手动查找循环 + limit。
let mut parts: Vec<Vec<u16>> = Vec::new();
let mut start = 0;
loop {
if parts.len() >= limit {
if parts.len() >= lim {
break;
}
match find_units(&s, &sep_units, start) {
+1 -1
View File
@@ -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),
@@ -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();
@@ -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]