diff --git a/crates/oxide_builtins/src/number.rs b/crates/oxide_builtins/src/number.rs index a25f1743..55e83287 100644 --- a/crates/oxide_builtins/src/number.rs +++ b/crates/oxide_builtins/src/number.rs @@ -75,63 +75,194 @@ pub fn number_is_finite(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(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::() { + Ok(v) => v, + Err(_) => return NativeResult::Ok(JsValue::float(f64::NAN)), } } else { - 10 - }; - - 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 mut acc = 0.0f64; + for c in prefix.chars() { + acc = acc * radix as f64 + c.to_digit(radix).unwrap() as f64; } - } else { - (s.to_string(), false) + acc }; - 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)); + 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 { + 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(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::(&s) { - Ok(v) => NativeResult::Ok(JsValue::float(v)), + match fast_float::parse::(&rest[..end]) { + Ok(v) => NativeResult::Ok(JsValue::float(if neg { -v } else { v })), Err(_) => NativeResult::Ok(JsValue::float(f64::NAN)), } } diff --git a/crates/oxide_vm/tests/builtins_number_tests.rs b/crates/oxide_vm/tests/builtins_number_tests.rs index 691eaca6..9cbcaa24 100644 --- a/crates/oxide_vm/tests/builtins_number_tests.rs +++ b/crates/oxide_vm/tests/builtins_number_tests.rs @@ -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();