fix: parseInt/parseFloat 前缀解析与 radix 语义符合规范
This commit is contained in:
@@ -75,63 +75,194 @@ pub fn number_is_finite<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
NativeResult::Ok(JsValue::bool(n.is_finite()))
|
||||
}
|
||||
|
||||
/// `parseInt(string, radix)`:按指定进制解析整数前缀;支持 `0x` 前缀,
|
||||
/// 空串或非法前缀返回 NaN。radix 为 0 或缺省时按 10 进制(`0x` 前缀除外)。
|
||||
/// ECMA-262 WhiteSpace / LineTerminator 判定(TrimString 用)。
|
||||
///
|
||||
/// 与 Rust `char::is_whitespace` 的差异:规范集合不含 U+0085(NEL),手工
|
||||
/// 按白名单匹配避免误剥。
|
||||
fn is_js_ws(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{1680}' | '\u{2000}'
|
||||
..='\u{200A}'
|
||||
| '\u{202F}'
|
||||
| '\u{205F}'
|
||||
| '\u{3000}'
|
||||
| '\u{FEFF}'
|
||||
| '\u{000A}'
|
||||
| '\u{000D}'
|
||||
| '\u{2028}'
|
||||
| '\u{2029}'
|
||||
)
|
||||
}
|
||||
|
||||
/// ECMA-262 ToInt32(§7.1.6):f64 → mod 2^32 回绕的有符号 i32。
|
||||
///
|
||||
/// NaN/±0/±∞ 归 0;先截断再对 2^32 取余(余数非负),保证超大输入
|
||||
/// (如 2^40 → 0)按回绕而非饱和。
|
||||
fn to_int32(n: f64) -> i32 {
|
||||
if n.is_nan() || n.is_infinite() || n == 0.0 {
|
||||
return 0;
|
||||
}
|
||||
(n.trunc().rem_euclid(4294967296.0) as u32) as i32
|
||||
}
|
||||
|
||||
/// `parseInt(string, radix)`:按 ECMA-262 §19.2.5 前缀解析。
|
||||
///
|
||||
/// 规范白名单 trim 后读 `+`/`-` 符号;radix 经 ToInt32(mod 2^32 回绕,
|
||||
/// NaN/undefined → 0),R≠0 且不在 [2,36] 返回 NaN;仅当原 R 为 0 或 16 时
|
||||
/// `0x`/`0X` 前缀按十六进制剥除。随后按进制收集最长连续有效数字前缀并转
|
||||
/// f64(十进制正确舍入、其余进制数学累加,均允许超 2^53 舍入),无有效
|
||||
/// 数字返回 NaN,结果在 i32 域内用 int 表示,`-0` 保留负零。
|
||||
pub fn number_parse_int<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::float(f64::NAN));
|
||||
}
|
||||
let s = oxide_runtime_api::to_string(vm.reg(args[1]));
|
||||
let s = s.trim();
|
||||
let s = s.trim_start_matches(is_js_ws).trim_end_matches(is_js_ws);
|
||||
|
||||
if s.is_empty() {
|
||||
// radix 经 ToInt32:缺省参数视为 undefined(ToInt32 → 0)。
|
||||
let raw = if args.len() > 2 {
|
||||
match vm.coerce_number_bounded(vm.reg(args[2])) {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
// coercion 触发用户 valueOf/toString 抛出的异常经
|
||||
// last_uncaught_value 恢复后原样重新抛出。
|
||||
if let Some(exc) = vm.take_uncaught_value() {
|
||||
return NativeResult::Err(exc);
|
||||
}
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert radix to a number"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
let r = to_int32(raw);
|
||||
|
||||
// 读符号并跳过;R≠0 且越出 [2,36] 直接 NaN(保留 0 参与后续 0x 判定)。
|
||||
let (neg, rest) = match s.as_bytes().first() {
|
||||
Some(b'-') => (true, &s[1..]),
|
||||
Some(b'+') => (false, &s[1..]),
|
||||
_ => (false, s),
|
||||
};
|
||||
let radix_default: u32 = if r == 0 {
|
||||
10
|
||||
} else if (2..=36).contains(&r) {
|
||||
r as u32
|
||||
} else {
|
||||
return NativeResult::Ok(JsValue::float(f64::NAN));
|
||||
};
|
||||
|
||||
// 0x/0X 前缀:仅当原 R 为 0(缺省/undefined/NaN/0)或 16 时剥前缀转十六进制。
|
||||
let (digits, radix) = if (r == 0 || r == 16)
|
||||
&& rest.len() >= 2
|
||||
&& rest.as_bytes()[0] == b'0'
|
||||
&& (rest.as_bytes()[1] == b'x' || rest.as_bytes()[1] == b'X')
|
||||
{
|
||||
(&rest[2..], 16u32)
|
||||
} else {
|
||||
(rest, radix_default)
|
||||
};
|
||||
|
||||
// 按进制收集最长连续有效数字前缀;十进制交 Rust 正确舍入解析
|
||||
// (逐位 f64 累加对 20+ 位数字会产生 1 ulp 级偏差),其余进制
|
||||
// 数学累加(2 的幂进制在 53 位内精确,超出按规范允许近似舍入)。
|
||||
let mut end = 0usize;
|
||||
for (i, c) in digits.char_indices() {
|
||||
if c.to_digit(radix).is_none() {
|
||||
break;
|
||||
}
|
||||
end = i + c.len_utf8();
|
||||
}
|
||||
if end == 0 {
|
||||
return NativeResult::Ok(JsValue::float(f64::NAN));
|
||||
}
|
||||
|
||||
let radix = if args.len() > 2 {
|
||||
let r = vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as i32;
|
||||
if r == 0 {
|
||||
10
|
||||
} else {
|
||||
r.clamp(2, 36)
|
||||
let prefix = &digits[..end];
|
||||
let acc = if radix == 10 {
|
||||
match prefix.parse::<f64>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return NativeResult::Ok(JsValue::float(f64::NAN)),
|
||||
}
|
||||
} else {
|
||||
10
|
||||
let mut acc = 0.0f64;
|
||||
for c in prefix.chars() {
|
||||
acc = acc * radix as f64 + c.to_digit(radix).unwrap() as f64;
|
||||
}
|
||||
acc
|
||||
};
|
||||
|
||||
let (rest, hex) = if s.starts_with("0x") || s.starts_with("0X") {
|
||||
if radix == 16 || radix == 0 || (args.len() <= 2) {
|
||||
(s[2..].to_string(), true)
|
||||
} else {
|
||||
(s.to_string(), false)
|
||||
let acc = if neg { -acc } else { acc };
|
||||
if acc == 0.0 {
|
||||
// 负零保留符号(parseInt("-0") → -0)。
|
||||
return NativeResult::Ok(JsValue::float(if neg { -0.0 } else { 0.0 }));
|
||||
}
|
||||
if acc.fract() == 0.0 && acc >= i32::MIN as f64 && acc <= i32::MAX as f64 {
|
||||
NativeResult::Ok(JsValue::int(acc as i32))
|
||||
} else {
|
||||
(s.to_string(), false)
|
||||
};
|
||||
|
||||
let actual_radix = if hex { 16u32 } else { radix as u32 };
|
||||
|
||||
if let Ok(n) = i32::from_str_radix(&rest, actual_radix) {
|
||||
return NativeResult::Ok(JsValue::int(n));
|
||||
NativeResult::Ok(JsValue::float(acc))
|
||||
}
|
||||
|
||||
NativeResult::Ok(JsValue::float(f64::NAN))
|
||||
}
|
||||
|
||||
/// `parseFloat(string)`:解析尽可能长的十进制浮点前缀;无法解析返回 NaN。
|
||||
/// `parseFloat(string)`:按 ECMA-262 §19.2.4 前缀解析。
|
||||
///
|
||||
/// 规范白名单 trim 后读 `+`/`-` 符号,特判精确大小写的 `Infinity`;随后按
|
||||
/// StrDecimalLiteral 文法扫描最长合法十进制前缀(整数 + 可选小数 + 可选
|
||||
/// 指数,`0x10` 在 'x' 处停止得 0,`1.2.3` 得 1.2),前缀子串交给
|
||||
/// fast_float 解析(溢出归 ±Infinity),无合法前缀返回 NaN。
|
||||
pub fn number_parse_float<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::float(f64::NAN));
|
||||
}
|
||||
let s = oxide_runtime_api::to_string(vm.reg(args[1]));
|
||||
let s = s.trim();
|
||||
let s = s.trim_start_matches(is_js_ws).trim_end_matches(is_js_ws);
|
||||
|
||||
if s.is_empty() {
|
||||
// 读符号;`Infinity` 大小写敏感,其后可带任意后缀(取最长合法前缀)。
|
||||
let (neg, rest) = match s.as_bytes().first() {
|
||||
Some(b'-') => (true, &s[1..]),
|
||||
Some(b'+') => (false, &s[1..]),
|
||||
_ => (false, s),
|
||||
};
|
||||
if rest.starts_with("Infinity") {
|
||||
return NativeResult::Ok(JsValue::float(if neg { f64::NEG_INFINITY } else { f64::INFINITY }));
|
||||
}
|
||||
|
||||
// 扫描 mantissa:数字与至多一个 '.'('.' 后必须有数字,整数部分可为空)。
|
||||
let b = rest.as_bytes();
|
||||
let mut i = 0usize;
|
||||
let mut dot = false;
|
||||
let mut mantissa_digits = 0usize;
|
||||
let mut end = 0usize;
|
||||
while i < b.len() {
|
||||
let c = b[i];
|
||||
if c.is_ascii_digit() {
|
||||
mantissa_digits += 1;
|
||||
end = i + 1;
|
||||
i += 1;
|
||||
} else if c == b'.' && !dot {
|
||||
dot = true;
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 指数部分:e/E [+/-] 数字,且 mantissa 必须先有数字;指数无数字则不含 'e'。
|
||||
if i < b.len() && (b[i] == b'e' || b[i] == b'E') && mantissa_digits > 0 {
|
||||
let mut j = i + 1;
|
||||
if j < b.len() && (b[j] == b'+' || b[j] == b'-') {
|
||||
j += 1;
|
||||
}
|
||||
if j < b.len() && b[j].is_ascii_digit() {
|
||||
while j < b.len() && b[j].is_ascii_digit() {
|
||||
j += 1;
|
||||
}
|
||||
end = j;
|
||||
}
|
||||
}
|
||||
if mantissa_digits == 0 {
|
||||
return NativeResult::Ok(JsValue::float(f64::NAN));
|
||||
}
|
||||
|
||||
match fast_float::parse::<f64, _>(&s) {
|
||||
Ok(v) => NativeResult::Ok(JsValue::float(v)),
|
||||
match fast_float::parse::<f64, _>(&rest[..end]) {
|
||||
Ok(v) => NativeResult::Ok(JsValue::float(if neg { -v } else { v })),
|
||||
Err(_) => NativeResult::Ok(JsValue::float(f64::NAN)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,84 @@ fn parse_float_invalid() {
|
||||
assert!(result.as_double().is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_int_prefix_and_radix_semantics() {
|
||||
let mut vm = Vm::new();
|
||||
let cases = [
|
||||
// 前缀解析:遇非法字符停止,取最长合法数字前缀。
|
||||
("parseInt('123abc') === 123", true),
|
||||
// +/- 符号前缀。
|
||||
("parseInt('+42') === 42", true),
|
||||
("parseInt('-42') === -42", true),
|
||||
// radix 显式时不触发 0x;缺省/0/16 时触发。
|
||||
("parseInt('0x10', 10) === 0", true),
|
||||
("parseInt('0x10') === 16", true),
|
||||
("parseInt('0x10', 0) === 16", true),
|
||||
("parseInt('0x10', 16) === 16", true),
|
||||
("parseInt('+0x10') === 16", true),
|
||||
("parseInt('0XFF') === 255", true),
|
||||
// 超 i32 用 f64 近似(允许舍入),i32 域内保持精确。
|
||||
("parseInt('99999999999999999999') === 1e20", true),
|
||||
("parseInt('9007199254740993') === 9007199254740992", true),
|
||||
("parseInt('2147483647') === 2147483647", true),
|
||||
("parseInt('2147483648') === 2147483648", true),
|
||||
// radix 有效字符过滤与空白处理。
|
||||
("parseInt('10', 2) === 2", true),
|
||||
("parseInt('1a', 2) === 1", true),
|
||||
("parseInt(' -42 ') === -42", true),
|
||||
// 负零保留符号。
|
||||
("1 / parseInt('-0') === -Infinity", true),
|
||||
// 无有效数字 / radix 越界 / 超大 radix 经 ToInt32 回绕。
|
||||
("isNaN(parseInt(''))", true),
|
||||
("isNaN(parseInt('0x'))", true),
|
||||
("isNaN(parseInt('0xG'))", true),
|
||||
("isNaN(parseInt('z'))", true),
|
||||
("isNaN(parseInt('10', 1))", true),
|
||||
("isNaN(parseInt('10', 37))", true),
|
||||
("isNaN(parseInt('z', 2 ** 40))", true),
|
||||
];
|
||||
for (src, expected) in cases {
|
||||
let result = eval(&mut vm, src).unwrap();
|
||||
assert_eq!(result.as_bool(), expected, "for {}", src);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_float_prefix_semantics() {
|
||||
let mut vm = Vm::new();
|
||||
let cases = [
|
||||
// 前缀解析:取最长合法 StrDecimalLiteral。
|
||||
("parseFloat('3.14abc') === 3.14", true),
|
||||
("parseFloat('1.2.3') === 1.2", true),
|
||||
("parseFloat('1e5') === 100000", true),
|
||||
("parseFloat('1e+5') === 100000", true),
|
||||
("parseFloat('.5') === 0.5", true),
|
||||
("parseFloat(' 3.14 ') === 3.14", true),
|
||||
// Infinity 大小写敏感,可带符号与后缀。
|
||||
("parseFloat('Infinity') === Infinity", true),
|
||||
("parseFloat('+Infinity') === Infinity", true),
|
||||
("parseFloat('-Infinity') === -Infinity", true),
|
||||
("parseFloat('Infinityx') === Infinity", true),
|
||||
("isNaN(parseFloat('-infinity'))", true),
|
||||
// 十六进制不解析,十进制前缀在 'x' 处停止;溢出归 Infinity。
|
||||
("parseFloat('0x10') === 0", true),
|
||||
("parseFloat('1e400') === Infinity", true),
|
||||
("parseFloat('-1e400') === -Infinity", true),
|
||||
// 无合法前缀。
|
||||
("isNaN(parseFloat(''))", true),
|
||||
("isNaN(parseFloat('.'))", true),
|
||||
("isNaN(parseFloat('.e5'))", true),
|
||||
("isNaN(parseFloat('abc'))", true),
|
||||
// 指数后无数字:'e' 不构成 ExponentPart,最长合法前缀为整数部分。
|
||||
("parseFloat('1e') === 1", true),
|
||||
("parseFloat('1e+') === 1", true),
|
||||
];
|
||||
for (src, expected) in cases {
|
||||
let result = eval(&mut vm, src).unwrap();
|
||||
assert_eq!(result.as_bool(), expected, "for {}", src);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_constructor() {
|
||||
let mut vm = Vm::new();
|
||||
|
||||
Reference in New Issue
Block a user