From 32c0474959e0bfdbff4b8dcc82b931f6d3521e07 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 8 Aug 2026 20:39:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Number::toString=20=E7=AC=A6=E5=90=88=20?= =?UTF-8?q?ECMA-262=EF=BC=881e21=20=E6=8C=87=E6=95=B0=E3=80=81radix=20Rang?= =?UTF-8?q?eError=E3=80=81parseInt/Number=20=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/json.rs | 5 +- crates/oxide_builtins/src/number.rs | 114 ++++++--- crates/oxide_runtime_api/src/lib.rs | 233 +++++++++++++++--- .../oxide_vm/tests/builtins_number_tests.rs | 53 ++++ 4 files changed, 325 insertions(+), 80 deletions(-) diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index bebf190d..9ab0536b 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -301,11 +301,8 @@ fn jsvalue_to_json( let n = val.as_double(); if !n.is_finite() { out.push_str("null"); - } else if n.fract() == 0.0 && n.abs() < 1e21 { - write!(out, "{}", n as i64).unwrap(); } else { - let mut buf = ryu::Buffer::new(); - out.push_str(buf.format(n)); + out.push_str(&oxide_runtime_api::js_number_to_string(n)); } } else if val.is_string() { let s = unsafe { (*val.as_string_ptr()).data.clone() }; diff --git a/crates/oxide_builtins/src/number.rs b/crates/oxide_builtins/src/number.rs index d6964266..62bfbc05 100644 --- a/crates/oxide_builtins/src/number.rs +++ b/crates/oxide_builtins/src/number.rs @@ -128,56 +128,83 @@ pub fn number_parse_float(vm: &mut H, args: &[u8]) -> NativeResult { } /// `Number.prototype.toString(radix)`:按指定进制(2..36)转字符串。 -/// 十进制走 ryu 快速格式化,NaN/Infinity 有专名输出。 +/// +/// 十进制走共享的 ECMA-262 Number::toString 格式化;非十进制对截断后的整数 +/// 部分做进制转换(小数部分按近似处理)。radix 经 ToInteger 后越界抛 RangeError, +/// NaN/Infinity 输出专名。 pub fn number_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN); let radix = if args.len() > 1 { - let r = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as u32; - r.clamp(2, 36) + let radix_arg = vm.reg(args[1]); + if radix_arg.is_undefined() { + 10u32 + } else { + // radix 走 ToIntegerOrInfinity:先经对象 coercion(poisoned valueOf + // 需传播其异常),NaN/±0 归 0 后越界抛 RangeError。 + let raw = match vm.coerce_number_bounded(radix_arg) { + 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")); + } + }; + let r = if raw.is_nan() || raw == 0.0 { + 0.0 + } else if raw.is_infinite() { + raw + } else { + raw.trunc() + }; + if !(2.0..=36.0).contains(&r) { + return NativeResult::Err(crate::error::create_range_error( + vm, + "toString() radix must be between 2 and 36", + )); + } + r as u32 + } } else { 10u32 }; if radix == 10 { - if n.is_nan() { - return NativeResult::Ok(vm.new_string("NaN")); - } - if n.is_infinite() { - if n.is_sign_positive() { - return NativeResult::Ok(vm.new_string("Infinity")); - } - return NativeResult::Ok(vm.new_string("-Infinity")); - } - if n.fract() == 0.0 && n >= i32::MIN as f64 && n <= i32::MAX as f64 { - return NativeResult::Ok(vm.new_string(&(n as i64).to_string())); - } - let mut buf = ryu::Buffer::new(); - NativeResult::Ok(vm.new_string(buf.format(n))) - } else { - if n.is_nan() { - return NativeResult::Ok(vm.new_string("NaN")); - } - let nn = n as i64; - let mut result = String::new(); - let mut value = nn.abs(); - if value == 0 { - result.push('0'); - } else { - let chars = "0123456789abcdefghijklmnopqrstuvwxyz"; - let mut digits = Vec::new(); - while value > 0 { - digits.push(chars.as_bytes()[(value % radix as i64) as usize] as char); - value /= radix as i64; - } - for ch in digits.iter().rev() { - result.push(*ch); - } - } - if nn < 0 { - result.insert(0, '-'); - } - NativeResult::Ok(vm.new_string(&result)) + return NativeResult::Ok(vm.new_string(&oxide_runtime_api::js_number_to_string(n))); } + + if n.is_nan() { + return NativeResult::Ok(vm.new_string("NaN")); + } + if n.is_infinite() { + return NativeResult::Ok(vm.new_string(if n.is_sign_positive() { "Infinity" } else { "-Infinity" })); + } + if n.abs() >= u128::MAX as f64 { + // 超出 u128 可精确表示的整数范围,退化为十进制近似。 + return NativeResult::Ok(vm.new_string(&oxide_runtime_api::js_number_to_string(n))); + } + let neg = n.is_sign_negative(); + let mut value = n.abs().trunc() as u128; + let mut result = String::new(); + if value == 0 { + result.push('0'); + } else { + let chars = b"0123456789abcdefghijklmnopqrstuvwxyz"; + let mut digits = Vec::new(); + while value > 0 { + digits.push(chars[(value % radix as u128) as usize] as char); + value /= radix as u128; + } + for ch in digits.iter().rev() { + result.push(*ch); + } + } + if neg { + result.insert(0, '-'); + } + NativeResult::Ok(vm.new_string(&result)) } /// `Number.prototype.toFixed(digits)`:固定小数位数(0..100)输出字符串, @@ -318,6 +345,11 @@ pub fn number_value_of(vm: &mut H, args: &[u8]) -> NativeResult { if this_val.is_object() { let ptr = this_val.as_js_object_ptr(); if !ptr.is_null() { + // Number.prototype 本身是 Number 对象,其 [[NumberData]] 为 +0。 + let number_proto = vm.session().builtin_world().number_proto.as_ptr() as *mut oxide_types::object::JsObject; + if ptr == number_proto { + return NativeResult::Ok(JsValue::int(0)); + } let obj = unsafe { &*ptr }; if obj.is_number_obj() { return NativeResult::Ok(obj.get_prop_at(0)); diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 716d673e..e4b3e5c9 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -182,7 +182,7 @@ pub fn to_number(val: JsValue) -> f64 { } if val.is_string() { let s = unsafe { string_data(val) }; - return s.parse::().unwrap_or(f64::NAN); + return parse_js_number(s); } if val.is_object() { return f64::NAN; @@ -190,6 +190,149 @@ pub fn to_number(val: JsValue) -> f64 { f64::NAN } +/// 按 ECMA-262 ToNumber 的 StringNumericLiteral 语法解析字符串。 +/// +/// 在 Rust `parse::` 之上补齐 JS 特有规则:空串为 0、`0x/0o/0b` 前缀按 +/// 对应进制解析、精确匹配 `±Infinity`。Rust parse 对 inf/nan 大小写不敏感, +/// 需先按十进制字符集排除这些令牌(JS 只接受精确的 "Infinity")。 +/// +/// # 边界与前提 +/// - trim 后空串 → 0 +/// - 十六/八/二进制要求全部字符为有效数字,含非法字符 → NaN +/// - 十进制结果与 Rust parse 一致,溢出时归 ±inf / 0 +fn parse_js_number(s: &str) -> f64 { + let t = s.trim(); + if t.is_empty() { + return 0.0; + } + let lower = t.to_ascii_lowercase(); + if let Some(digits) = lower.strip_prefix("0x") { + return parse_radix_int(digits, 16); + } + if let Some(digits) = lower.strip_prefix("0o") { + return parse_radix_int(digits, 8); + } + if let Some(digits) = lower.strip_prefix("0b") { + return parse_radix_int(digits, 2); + } + if t == "Infinity" || t == "+Infinity" { + return f64::INFINITY; + } + if t == "-Infinity" { + return f64::NEG_INFINITY; + } + // 十进制语法仅允许数字、符号、小数点与指数 e;含其它字符的令牌(如 + // inf/nan 变体)不是合法 StringNumericLiteral,一律 NaN。 + if !t + .chars() + .all(|c| c.is_ascii_digit() || c == '+' || c == '-' || c == '.' || c == 'e' || c == 'E') + { + return f64::NAN; + } + t.parse::().unwrap_or(f64::NAN) +} + +/// 解析指定进制的无符号整数;全部字符必须是有效数字,否则返回 NaN。 +/// +/// 累积在 f64 中:2/8/16 是 2 的幂,逐位乘加即为正确舍入结果;十进制不适用。 +fn parse_radix_int(s: &str, radix: u32) -> f64 { + if s.is_empty() { + return f64::NAN; + } + let mut v = 0.0f64; + for c in s.chars() { + match c.to_digit(radix) { + Some(d) => v = v * radix as f64 + d as f64, + None => return f64::NAN, + } + } + v +} + +/// 把 f64 格式化为 ECMA-262 Number::toString 的字符串(含 NaN/±Infinity 专名)。 +/// +/// 对有限非零数,先经 ryu 最短表示取出有效数字与十进制指数(`format_finite` +/// 输出形如 `"123.45"` / `"1e21"` / `"0.001"`),再按规范分段重建:指数 +/// `-6 < n <= 21` 时用定点表示,其余用科学计数法 `d.ddde±e`。替代原先 +/// `(d as i64)` 直接转换——后者对 `>= 2^63` 的数值溢出。 +/// +/// # 步骤 +/// 1. 取绝对值经 ryu `format_finite` 得最短表示 `s` +/// 2. 含 `e` 时解析出有效数字与指数,按四段规则重建(整数/带小数点/纯小数/科学计数) +/// 3. 不含 `e` 的定点形式仅需去掉整数末尾的 `.0` 后缀 +/// +/// # 边界与前提 +/// - NaN → "NaN",±∞ → "±Infinity",±0 → "0" +/// - 负数先格式化绝对值再加 "-" 前缀 +pub fn js_number_to_string(d: f64) -> String { + if d.is_nan() { + return "NaN".to_string(); + } + if d.is_infinite() { + return if d.is_sign_positive() { + "Infinity".to_string() + } else { + "-Infinity".to_string() + }; + } + if d == 0.0 { + return "0".to_string(); + } + let neg = d.is_sign_negative(); + let mut buf = ryu::Buffer::new(); + let s = buf.format_finite(d.abs()); + let mut out = String::new(); + if let Some(e_pos) = s.find('e') { + let digits: Vec = s[..e_pos].chars().filter(|c| *c != '.').collect(); + let exp: i32 = s[e_pos + 1..].parse().unwrap_or(0); + let k = digits.len() as i32; + let n = exp + 1; + if n > -6 && n <= 21 { + // 定点:ryu 在此范围输出指数形式但 JS 要求十进制。 + if n <= 0 { + out.push_str("0."); + for _ in 0..(-n) { + out.push('0'); + } + out.extend(digits.iter()); + } else if k <= n { + out.extend(digits.iter()); + for _ in 0..(n - k) { + out.push('0'); + } + } else { + out.extend(digits[..n as usize].iter()); + out.push('.'); + out.extend(digits[n as usize..].iter()); + } + } else { + // 科学计数:首位 + (可选 "." + 剩余) + e[+/-]指数。 + out.push(digits[0]); + if k > 1 { + out.push('.'); + out.extend(digits[1..].iter()); + } + out.push('e'); + if n > 1 { + out.push('+'); + } + out.push_str(&(n - 1).to_string()); + } + } else if let Some(stripped) = s.strip_suffix(".0") { + out.push_str(stripped); + } else { + out.push_str(s); + } + if neg { + let mut res = String::with_capacity(out.len() + 1); + res.push('-'); + res.push_str(&out); + res + } else { + out + } +} + /// ToUint32(ECMA-262 §7.1.6):对数值取模 2^32。NaN/±0/Infinity 归零。 pub fn to_uint32(val: JsValue) -> u32 { let n = to_number(val); @@ -218,22 +361,7 @@ pub fn push_to_string(val: JsValue, buf: &mut String) { return; } if val.is_double() { - let d = val.as_double(); - if d.is_nan() { - buf.push_str("NaN"); - return; - } - if d.is_infinite() { - buf.push_str(if d.is_sign_positive() { "Infinity" } else { "-Infinity" }); - return; - } - if d.is_finite() && d.fract() == 0.0 { - use std::fmt::Write; - let _ = write!(buf, "{}", d as i64); - return; - } - let mut ryubuf = ryu::Buffer::new(); - buf.push_str(ryubuf.format(d)); + buf.push_str(&js_number_to_string(val.as_double())); return; } if val.is_bool() { @@ -259,29 +387,14 @@ pub fn push_to_string(val: JsValue, buf: &mut String) { /// 把原始值转成字符串(ToString 的原始值路径)。 /// -/// 数值使用与 V8 一致的格式化(整数直接打印、有限数用 ryu 最短表示); -/// Object 在此返回占位符 `[object]`,完整路径见 [`to_string_full`]。 +/// 数值走 ECMA-262 Number::toString 格式化;Object 在此返回占位符 `[object]`, +/// 完整路径见 [`to_string_full`]。 pub fn to_string(val: JsValue) -> String { if val.is_int() { return val.as_int().to_string(); } if val.is_double() { - let d = val.as_double(); - if d.is_nan() { - return "NaN".to_string(); - } - if d.is_infinite() { - return if d.is_sign_positive() { - "Infinity".to_string() - } else { - "-Infinity".to_string() - }; - } - if d.is_finite() && d.fract() == 0.0 { - return (d as i64).to_string(); - } - let mut buf = ryu::Buffer::new(); - return buf.format(d).to_string(); + return js_number_to_string(val.as_double()); } if val.is_bool() { return val.as_bool().to_string(); @@ -638,3 +751,53 @@ pub fn to_string_full(val: JsValue, host: &mut H) -> Result String { + js_number_to_string(d) + } + + #[test] + fn number_to_string_boundaries() { + assert_eq!(fmt(1e21), "1e+21"); + assert_eq!(fmt(1e20), "100000000000000000000"); + assert_eq!(fmt(123.45), "123.45"); + assert_eq!(fmt(0.000001), "0.000001"); + assert_eq!(fmt(0.0000001), "1e-7"); + assert_eq!(fmt(1e15), "1000000000000000"); + assert_eq!(fmt(0.1 + 0.2), "0.30000000000000004"); + assert_eq!(fmt(0.0), "0"); + assert_eq!(fmt(-0.0), "0"); + assert_eq!(fmt(-123.45), "-123.45"); + assert_eq!(fmt(-1e21), "-1e+21"); + assert_eq!(fmt(1e16), "10000000000000000"); + assert_eq!(fmt(f64::MAX), "1.7976931348623157e+308"); + assert_eq!(fmt(5e-324), "5e-324"); + assert_eq!(fmt(1.5e20), "150000000000000000000"); + assert_eq!(fmt(f64::NAN), "NaN"); + assert_eq!(fmt(f64::INFINITY), "Infinity"); + assert_eq!(fmt(f64::NEG_INFINITY), "-Infinity"); + } + + #[test] + fn parse_js_number_rules() { + assert_eq!(parse_js_number(""), 0.0); + assert_eq!(parse_js_number(" "), 0.0); + assert_eq!(parse_js_number("0xa"), 10.0); + assert_eq!(parse_js_number("0X1f"), 31.0); + assert_eq!(parse_js_number("0b101"), 5.0); + assert_eq!(parse_js_number("0o17"), 15.0); + assert_eq!(parse_js_number("Infinity"), f64::INFINITY); + assert_eq!(parse_js_number("-Infinity"), f64::NEG_INFINITY); + assert!(parse_js_number("INFINITY").is_nan()); + assert!(parse_js_number("infinity").is_nan()); + assert!(parse_js_number("0x1g").is_nan()); + assert!(parse_js_number("-0x1").is_nan()); + assert_eq!(parse_js_number("1.5e3"), 1500.0); + assert_eq!(parse_js_number("-0"), -0.0); + assert!(parse_js_number("1abc").is_nan()); + } +} diff --git a/crates/oxide_vm/tests/builtins_number_tests.rs b/crates/oxide_vm/tests/builtins_number_tests.rs index a185c00c..852c1338 100644 --- a/crates/oxide_vm/tests/builtins_number_tests.rs +++ b/crates/oxide_vm/tests/builtins_number_tests.rs @@ -90,6 +90,59 @@ fn to_fixed_of_number() { assert_eq!(s, "3.14"); } +#[test] +fn to_string_scientific_boundaries() { + let mut vm = Vm::new(); + let cases = [ + ("(1e21).toString()", "1e+21"), + ("(1e20).toString()", "100000000000000000000"), + ("(123.45).toString()", "123.45"), + ("(0.000001).toString()", "0.000001"), + ("(0.0000001).toString()", "1e-7"), + ("(1e15).toString()", "1000000000000000"), + ("(0.1+0.2).toString()", "0.30000000000000004"), + ("String(1e21)", "1e+21"), + ("(\"\" + 1e21)", "1e+21"), + ("(-1e21).toString()", "-1e+21"), + ("JSON.stringify(1e21)", "1e+21"), + ("JSON.stringify(1e20)", "100000000000000000000"), + ]; + for (src, expected) in cases { + let result = eval(&mut vm, src).unwrap(); + let s = vm.lookup_str(result).unwrap_or_default(); + assert_eq!(s, expected, "for {}", src); + } +} + +#[test] +fn to_string_radix_and_range_error() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "(255).toString(16)").unwrap(); + assert_eq!(vm.lookup_str(result).unwrap_or_default(), "ff"); + + let result = eval(&mut vm, "(2).toString(2)").unwrap(); + assert_eq!(vm.lookup_str(result).unwrap_or_default(), "10"); + + let err = eval(&mut vm, "(10).toString(1)").unwrap_err(); + assert!(err.contains("RangeError"), "got: {}", err); + + let err = eval(&mut vm, "(10).toString(37)").unwrap_err(); + assert!(err.contains("RangeError"), "got: {}", err); +} + +#[test] +fn number_constructor_string_rules() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Number('0xa')").unwrap(); + assert_eq!(result.as_int(), 10); + + let result = eval(&mut vm, "Number('')").unwrap(); + assert_eq!(result.as_int(), 0); + + let result = eval(&mut vm, "Number('INFINITY')").unwrap(); + assert!(result.as_double().is_nan()); +} + #[test] fn number_is_integer_true() { let mut vm = Vm::new();