From 550d8f4cbab253e4914a525a4512464c175cd91f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 22 Sep 2026 09:59:53 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AF=B9=E9=BD=90=20Number=20=E5=8E=9F?= =?UTF-8?q?=E5=9E=8B=E5=9B=9B=E6=96=B9=E6=B3=95=E8=A7=84=E8=8C=83=E5=BA=8F?= =?UTF-8?q?=E4=BD=8D=E5=B9=B6=E8=A1=A5=E9=BD=90=E7=BB=91=E5=AE=9A=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/number.rs | 252 +++++++++++++----- crates/oxide_kernel/src/builtin/construct.rs | 13 + crates/oxide_kernel/src/builtin/rebuild.rs | 10 +- crates/oxide_vm/src/bindings/bind_number.rs | 26 +- .../oxide_vm/tests/builtins_number_tests.rs | 133 ++++++++- 5 files changed, 367 insertions(+), 67 deletions(-) diff --git a/crates/oxide_builtins/src/number.rs b/crates/oxide_builtins/src/number.rs index 9b36cd2c..205b4c9b 100644 --- a/crates/oxide_builtins/src/number.rs +++ b/crates/oxide_builtins/src/number.rs @@ -2,6 +2,19 @@ use oxide_types::value::JsValue; use oxide_runtime_api::{NativeResult, VmHost}; +/// 整值且 i32 域内的有限数转 int 表示;-0 排除 +/// (int 表示丢符号,规范要求 `Number(-0)` 得 -0)。 +fn i32_of_integral(n: f64) -> Option { + if n == 0.0 && n.is_sign_negative() { + return None; + } + if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { + Some(n as i32) + } else { + None + } +} + /// JS `Number()` 构造逻辑:把参数按 ToNumber 语义转换。 /// 普通调用返回原始 number(整数走 int 表示);new 语义返回 `[[NumberData]]` 包装对象。 pub fn number_constructor(vm: &mut H, args: &[u8]) -> NativeResult { @@ -55,38 +68,42 @@ pub fn number_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let this_val = vm.reg(args[0]); let obj = unsafe { &mut *this_val.as_js_object_ptr() }; obj.type_tag = oxide_types::object::JsObject::OBJ_TYPE_NUMBER_OBJ; - let boxed = if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { - JsValue::int(n as i32) - } else { - JsValue::float(n) - }; + let boxed = i32_of_integral(n).map(JsValue::int).unwrap_or_else(|| JsValue::float(n)); obj.set_boxed_value(boxed); return NativeResult::Ok(this_val); } - if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { - NativeResult::Ok(JsValue::int(n as i32)) - } else { - NativeResult::Ok(JsValue::float(n)) - } + NativeResult::Ok(i32_of_integral(n).map(JsValue::int).unwrap_or_else(|| JsValue::float(n))) } /// `Number.isNaN`:参数严格等于 NaN 才返回 true(不做隐式类型转换)。 +/// +/// 步 1 为严格判型:非 Number 原始类型(含 Number 对象)直接 false, +/// 不走 ToNumber 强转(否则 "NaN"/对象装箱会误报 true)。 pub fn number_is_nan(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::bool(false)); } - let n = oxide_runtime_api::to_number(vm.reg(args[1])); - NativeResult::Ok(JsValue::bool(n.is_nan())) + let val = vm.reg(args[1]); + if !val.is_int() && !val.is_double() { + return NativeResult::Ok(JsValue::bool(false)); + } + NativeResult::Ok(JsValue::bool(oxide_runtime_api::to_number(val).is_nan())) } /// `Number.isFinite`:参数为有限数才返回 true(不做隐式类型转换)。 +/// +/// 步 1 为严格判型:非 Number 原始类型(含 Number 对象)直接 false, +/// 不走 ToNumber 强转(否则 "1" 等会误报 true)。 pub fn number_is_finite(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::bool(false)); } - let n = oxide_runtime_api::to_number(vm.reg(args[1])); - NativeResult::Ok(JsValue::bool(n.is_finite())) + let val = vm.reg(args[1]); + if !val.is_int() && !val.is_double() { + return NativeResult::Ok(JsValue::bool(false)); + } + NativeResult::Ok(JsValue::bool(oxide_runtime_api::to_number(val).is_finite())) } /// 判定 parseInt/parseFloat 修剪时要剥掉的空白字符(规范 WhiteSpace 与 @@ -303,13 +320,75 @@ pub fn number_parse_float(vm: &mut H, args: &[u8]) -> NativeResult { } } +/// thisNumberValue(spec 21.7.3 共用步 1):Number 原始值直返;带 +/// [[NumberData]] 的 Number 对象取被包值;其余一律 TypeError。 +/// toFixed/toExponential/toPrecision/toString 四方法均以本助手起步, +/// 取代 ToObject 语义的 coerce(后者对 {} 静默装箱,不抛错)。 +fn this_number_value(vm: &mut H, this_val: JsValue) -> Result { + if this_val.is_int() || this_val.is_double() { + return Ok(oxide_runtime_api::to_number(this_val)); + } + if this_val.is_object() { + let ptr = this_val.as_js_object_ptr(); + if !ptr.is_null() { + // Number.prototype 本身即 [[NumberData]] = +0 的 Number 对象; + // 与 number_value_of 的 proto 特判同形。 + let number_proto = vm.session().builtin_world().number_proto.as_ptr() as *mut oxide_types::object::JsObject; + if std::ptr::eq(ptr, number_proto) { + return Ok(0.0); + } + let obj = unsafe { &*ptr }; + if obj.is_number_obj() { + return Ok(oxide_runtime_api::to_number(obj.boxed_value())); + } + } + } + Err(crate::error::create_type_error(vm, "Cannot convert this value to a number")) +} + +/// f/p/radix 参数转换:完整 ToNumber(对象执行 ToPrimitive 且 valueOf/toString +/// 抛出的异常原样传播)后走 ToIntegerOrInfinity。纯核 `to_integer_or_infinity` +/// 不传播异常,不得用于参数位。 +fn coerce_to_integer_or_infinity(vm: &mut H, arg: JsValue) -> Result { + let raw = match vm.coerce_number_bounded(arg) { + Ok(n) => n, + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + return Err(crate::error::create_type_error(vm, "Cannot convert argument to a number")); + } + }; + if raw.is_nan() || raw == 0.0 { + Ok(0.0) + } else if raw.is_infinite() { + Ok(raw) + } else { + Ok(raw.trunc()) + } +} + +/// `Number.prototype.toLocaleString()`:locale 参数忽略,恒按十进制输出。 +/// 与 toString 的 radix-10 路径同形;不读第二个实参(length 为 0)。 +pub fn number_to_locale_string(vm: &mut H, args: &[u8]) -> NativeResult { + let n = match this_number_value(vm, vm.reg(args[0])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + }; + NativeResult::Ok(vm.new_string_owned(oxide_runtime_api::js_number_to_string(n))) +} + /// `Number.prototype.toString(radix)`:按指定进制(2..36)转字符串。 /// -/// 十进制走共享的 ECMA-262 Number::toString 格式化;非十进制对截断后的整数 -/// 部分做进制转换(小数部分按近似处理)。radix 经 ToInteger 后越界抛 RangeError, +/// 步 1 先 thisNumberValue(非 Number 原始值/对象抛 TypeError);十进制走共享的 +/// 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 n = match this_number_value(vm, vm.reg(args[0])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + }; let radix = if args.len() > 1 { let radix_arg = vm.reg(args[1]); if radix_arg.is_undefined() { @@ -385,28 +464,43 @@ pub fn number_to_string(vm: &mut H, args: &[u8]) -> NativeResult { /// `Number.prototype.toFixed(digits)`:固定小数位数(0..100)输出字符串, /// 超出范围抛 RangeError;NaN/Infinity 输出专名。 +/// +/// 步序:先 thisNumberValue,再转换 fractionDigits 并做范围校验(范围检查 +/// 先于 NaN 短路,NaN.toFixed(Infinity) 须抛 RangeError);随后 x ≥ 10^21 +/// 退化为 String(x) 科学式(±Infinity 同形,经此分支输出专名)。 pub fn number_to_fixed(vm: &mut H, args: &[u8]) -> NativeResult { - let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN); + let n = match this_number_value(vm, vm.reg(args[0])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + }; + let raw = if args.len() > 1 { + match coerce_to_integer_or_infinity(vm, vm.reg(args[1])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + } + } else { + 0.0 + }; + if !(0.0..=100.0).contains(&raw) { + return NativeResult::Err(crate::error::create_range_error( + vm, + "toFixed() fractionDigits must be between 0 and 100", + )); + } 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" })); + // x ≥ 10^21 用科学式 String 形态(±Infinity 同为该形态)。 + if n.abs() >= 1e21 { + let s = if n < 0.0 { + format!("-{}", oxide_runtime_api::js_number_to_string(-n)) + } else { + oxide_runtime_api::js_number_to_string(n) + }; + return NativeResult::Ok(vm.new_string_owned(s)); } - let fraction_digits = if args.len() > 1 { - let raw = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])); - if !(0.0..=100.0).contains(&raw) { - return NativeResult::Err(crate::error::create_range_error( - vm, - "toFixed() fractionDigits must be between 0 and 100", - )); - } - raw as usize - } else { - 0usize - }; let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n }; - let formatted = format!("{:.precision$}", n_abs, precision = fraction_digits); + let formatted = format!("{:.precision$}", n_abs, precision = raw as usize); NativeResult::Ok(vm.new_string(&formatted)) } @@ -433,15 +527,28 @@ pub fn number_is_safe_integer(vm: &mut H, args: &[u8]) -> NativeResul /// `Number.prototype.toPrecision(precision)`:按有效数字位数(1..100)输出, /// 科学计数法与定点表示按指数自动切换,超范围抛 RangeError。 +/// +/// 步序:先 thisNumberValue;precision 缺省直接返 ToString(x)(含 NaN/±∞ +/// 全形);再转 precision,NaN/±∞ 专名先于范围校验((±∞).toPrecision(1000) +/// 返 "Infinity" 而非 RangeError),最后舍入渲染(指数恒带符号)。 pub fn number_to_precision(vm: &mut H, args: &[u8]) -> NativeResult { - if args.len() <= 1 { - return NativeResult::Err(crate::error::create_range_error( - vm, - "toPrecision() requires a precision argument between 1 and 100", - )); + let n = match this_number_value(vm, vm.reg(args[0])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + }; + if args.len() <= 1 || vm.reg(args[1]).is_undefined() { + return NativeResult::Ok(vm.new_string_owned(oxide_runtime_api::js_number_to_string(n))); + } + let raw = match coerce_to_integer_or_infinity(vm, vm.reg(args[1])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + }; + 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" })); } - let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN); - let raw = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])); if !(1.0..=100.0).contains(&raw) { return NativeResult::Err(crate::error::create_range_error( vm, @@ -449,12 +556,6 @@ pub fn number_to_precision(vm: &mut H, args: &[u8]) -> NativeResult { )); } let precision = raw as usize; - 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" })); - } let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n.abs() }; let is_neg = n.is_sign_negative() && !(n == 0.0); let e = if n_abs == 0.0 { 0i32 } else { n_abs.log10().floor() as i32 }; @@ -462,7 +563,14 @@ pub fn number_to_precision(vm: &mut H, args: &[u8]) -> NativeResult { let dec = ((precision as i32 - e - 1).max(0)) as usize; format!("{:.dec$}", n_abs, dec = dec) } else { - format!("{:.dec$e}", n_abs, dec = precision - 1) + // Rust 的 {:e} 指数不带符号,规范要求恒带 +/-。 + let mut s = format!("{:.dec$e}", n_abs, dec = precision - 1); + if let Some(e_pos) = s.find('e') { + if !s[e_pos + 1..].starts_with('-') { + s.insert(e_pos + 1, '+'); + } + } + s }; if is_neg { NativeResult::Ok(vm.new_string(&format!("-{}", formatted))) @@ -472,18 +580,39 @@ pub fn number_to_precision(vm: &mut H, args: &[u8]) -> NativeResult { } /// `Number.prototype.toExponential(digits)`:按科学计数法输出; -/// digits 缺省时自动决定小数位数,超范围抛 RangeError。 +/// digits 缺省/undefined 取最短精确有效数字数,超范围抛 RangeError。 +/// +/// 步序:先 thisNumberValue,再转换 fractionDigits(对象参数执行 ToPrimitive +/// 且异常原样传播);NaN/±∞ 专名先于范围检查;指数恒带符号。 pub fn number_to_exponential(vm: &mut H, args: &[u8]) -> NativeResult { - let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN); + let n = match this_number_value(vm, vm.reg(args[0])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + }; + let raw = if args.len() > 1 && !vm.reg(args[1]).is_undefined() { + match coerce_to_integer_or_infinity(vm, vm.reg(args[1])) { + Ok(v) => v, + Err(exc) => return NativeResult::Err(exc), + } + } else { + f64::NAN + }; 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" })); } + let has_digits = !raw.is_nan(); + if has_digits && !(0.0..=100.0).contains(&raw) { + return NativeResult::Err(crate::error::create_range_error( + vm, + "toExponential() fractionDigits must be between 0 and 100", + )); + } let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n.abs() }; - let sign_prefix = if n.is_sign_negative() && !n.is_nan() && !(n == 0.0) { "-" } else { "" }; - if args.len() <= 1 { + let sign_prefix = if n < 0.0 { "-" } else { "" }; + if !has_digits { let formatted = format!("{:e}", n_abs); let mut s = formatted; if let Some(e_pos) = s.find('e') { @@ -495,20 +624,23 @@ pub fn number_to_exponential(vm: &mut H, args: &[u8]) -> NativeResult if mantissa.contains('.') { s = format!("{}{}", mantissa, &s[e_pos..]); } + // 指数恒带符号(Rust 正指数不输出 +)。 + if !s[e_pos + 1..].starts_with('-') { + s.insert(e_pos + 1, '+'); + } } let result = format!("{}{}", sign_prefix, s); return NativeResult::Ok(vm.new_string_owned(result)); } - let raw = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])); - if !(0.0..=100.0).contains(&raw) { - return NativeResult::Err(crate::error::create_range_error( - vm, - "toExponential() fractionDigits must be between 0 and 100", - )); + let formatted = format!("{:.digits$e}", n_abs, digits = raw as usize); + // 指数恒带符号(Rust 正指数不输出 +)。 + let mut s = formatted; + if let Some(e_pos) = s.find('e') { + if !s[e_pos + 1..].starts_with('-') { + s.insert(e_pos + 1, '+'); + } } - let fraction_digits = raw as usize; - let formatted = format!("{:.digits$e}", n_abs, digits = fraction_digits); - NativeResult::Ok(vm.new_string(&format!("{}{}", sign_prefix, formatted))) + NativeResult::Ok(vm.new_string(&format!("{}{}", sign_prefix, s))) } /// `Number.prototype.valueOf`:返回包装对象的原始 number; diff --git a/crates/oxide_kernel/src/builtin/construct.rs b/crates/oxide_kernel/src/builtin/construct.rs index 3e8a870e..2328fa86 100644 --- a/crates/oxide_kernel/src/builtin/construct.rs +++ b/crates/oxide_kernel/src/builtin/construct.rs @@ -75,6 +75,18 @@ pub(crate) fn make_named_pair( make_pair(string_forge, shape_forge, name, labels.prototype, labels.constructor, labels.name) } +/// 把 Number.prototype 标成 Number 对象本体:规范 21.7.3 规定其是 +/// [[NumberData]] = +0 的 Number object。type_tag 是 Object.prototype.toString +/// 品牌表的判据(置位后报 "[object Number]"),boxed 值供 thisNumberValue/ +/// valueOf 的通用分支读取。 +pub(crate) fn tag_number_proto(proto: &P) { + let ptr = proto.as_ptr() as *mut JsObject; + // SAFETY: proto 是 make_named_pair 刚建的本进程对象,P 引用与裸指针同址。 + let obj = unsafe { &mut *ptr }; + obj.type_tag = JsObject::OBJ_TYPE_NUMBER_OBJ; + obj.set_boxed_value(JsValue::int(0)); +} + pub(crate) fn make_error_subtypes(error_proto: &P) -> ErrorSubtypeProtos { let error_proto_val = JsValue::from_js_object(error_proto.as_ptr() as *mut JsObject); ErrorSubtypeProtos { @@ -436,6 +448,7 @@ impl BuiltinWorld { let (function_proto, function_constructor) = make_named_pair(string_forge, shape_forge, labels, "Function"); let (string_proto, string_constructor) = make_named_pair(string_forge, shape_forge, labels, "String"); let (number_proto, number_constructor) = make_named_pair(string_forge, shape_forge, labels, "Number"); + tag_number_proto(&number_proto); let (boolean_proto, boolean_constructor) = make_named_pair(string_forge, shape_forge, labels, "Boolean"); let (error_proto, error_constructor) = make_named_pair(string_forge, shape_forge, labels, "Error"); let (symbol_proto, symbol_constructor) = make_named_pair(string_forge, shape_forge, labels, "Symbol"); diff --git a/crates/oxide_kernel/src/builtin/rebuild.rs b/crates/oxide_kernel/src/builtin/rebuild.rs index 58ab7a0e..56db6f86 100644 --- a/crates/oxide_kernel/src/builtin/rebuild.rs +++ b/crates/oxide_kernel/src/builtin/rebuild.rs @@ -7,8 +7,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use super::construct::{ - builtin_labels, make_error_subtypes, make_named_pair, make_typed_array_family, wire_builtin_world_links, - ErrorSubtypeProtos, TypedArrayFamily, + builtin_labels, make_error_subtypes, make_named_pair, make_typed_array_family, tag_number_proto, + wire_builtin_world_links, ErrorSubtypeProtos, TypedArrayFamily, }; use super::BuiltinWorld; use crate::kernel::BuiltinDirtySet; @@ -183,7 +183,11 @@ impl BuiltinWorld { (current.string_proto.clone(), current.string_constructor.clone()) }; let (number_proto, number_constructor) = if dirty.number { - make_named_pair(string_forge, shape_forge, labels, "Number") + // 脏重建与全量构造同形:原型本体须带 Number 对象 tag 与 +0 包值, + // 漏此分支 full_reset 后 tag 翻回 "Object"。 + let (proto, ctor) = make_named_pair(string_forge, shape_forge, labels, "Number"); + tag_number_proto(&proto); + (proto, ctor) } else { (current.number_proto.clone(), current.number_constructor.clone()) }; diff --git a/crates/oxide_vm/src/bindings/bind_number.rs b/crates/oxide_vm/src/bindings/bind_number.rs index 1b618278..2d9781be 100644 --- a/crates/oxide_vm/src/bindings/bind_number.rs +++ b/crates/oxide_vm/src/bindings/bind_number.rs @@ -38,16 +38,23 @@ pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut proto, core, &[ - ("toString", oxide_builtins::number::number_to_string:: as *const (), 0), - ("toFixed", oxide_builtins::number::number_to_fixed:: as *const (), 0), + ("toString", oxide_builtins::number::number_to_string:: as *const (), 1), + ("toFixed", oxide_builtins::number::number_to_fixed:: as *const (), 1), ( "toExponential", oxide_builtins::number::number_to_exponential:: as *const (), - 0, + 1, ), ( "toPrecision", oxide_builtins::number::number_to_precision:: as *const (), + 1, + ), + // toLocaleString 为 Number.prototype 的 own 属性(规范 21.7.3.25): + // 保留参数不计 length(length 0),locale 参数忽略、恒十进制。 + ( + "toLocaleString", + oxide_builtins::number::number_to_locale_string:: as *const (), 0, ), ("valueOf", oxide_builtins::number::number_value_of:: as *const (), 0), @@ -85,4 +92,17 @@ pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut ("isFinite", oxide_builtins::global::global_is_finite:: as *const (), 1), ], ); + + // 规范(21.7.3 / 20.1.3 注):Number.parseInt/parseFloat 与全局同名函数 + // 是同一函数对象(SameValue)。ctor 侧绑定先执行、产生独立 wrapper, + // 此处把 global 侧 wrapper 值原位写回 ctor 同名槽,描述符形态不变。 + for name in ["parseInt", "parseFloat"] { + let si = core.perm_interner().intern(name).0; + if let Some(global_pos) = core.shape_forge().lookup_position(global.shape_id(), si) { + let val = global.get_prop_at(global_pos); + if let Some(ctor_pos) = core.shape_forge().lookup_position(ctor.shape_id(), si) { + ctor.set_prop_at(ctor_pos, val); + } + } + } } diff --git a/crates/oxide_vm/tests/builtins_number_tests.rs b/crates/oxide_vm/tests/builtins_number_tests.rs index 65f6ced6..a9854988 100644 --- a/crates/oxide_vm/tests/builtins_number_tests.rs +++ b/crates/oxide_vm/tests/builtins_number_tests.rs @@ -282,7 +282,8 @@ fn number_to_exponential() { let mut vm = Vm::new(); let result = eval(&mut vm, "var n = 123.456; n.toExponential(2)").unwrap(); let s = vm.lookup_str(result).unwrap_or_default(); - assert_eq!(s, "1.23e2"); + // 指数恒带符号(规范科学式形态)。 + assert_eq!(s, "1.23e+2"); } #[test] @@ -316,3 +317,133 @@ fn boxed_number_is_object_and_call_stays_primitive() { let n = eval(&mut vm, "Number('7')").unwrap(); assert_eq!(n.as_int(), 7); } + +fn str_of(vm: &mut Vm, source: &str) -> String { + let r = eval(vm, source).unwrap(); + vm.lookup_str(r).unwrap_or_default() +} + +#[test] +fn number_proto_methods_this_number_value() { + let mut vm = Vm::new(); + // Number 原始值与 Number 对象均合法 this。 + assert_eq!(str_of(&mut vm, "(new Number(7)).toFixed(1)"), "7.0"); + assert_eq!(str_of(&mut vm, "(new Number(7)).toPrecision(2)"), "7.0"); + assert_eq!(str_of(&mut vm, "(new Number(7)).toExponential(1)"), "7.0e+0"); + assert_eq!(str_of(&mut vm, "(new Number(255)).toString(16)"), "ff"); + // Number.prototype 本体是 [[NumberData]] = +0 的 Number 对象。 + assert_eq!(str_of(&mut vm, "Number.prototype.toFixed(1)"), "0.0"); + assert_eq!(str_of(&mut vm, "Number.prototype.toExponential(0)"), "0e+0"); + // 其余 this 一律 TypeError(四方法非通用)。 + for method in ["toString", "toFixed", "toExponential", "toPrecision"] { + let src = format!("Number.prototype.{}.call({{}})", method); + let err = eval(&mut vm, &src).unwrap_err(); + assert!(err.contains("TypeError"), "for {}: {}", method, err); + } + let err = eval(&mut vm, "Number.prototype.toFixed.call(new String('1'))").unwrap_err(); + assert!(err.contains("TypeError"), "got: {}", err); + let err = eval(&mut vm, "Number.prototype.toPrecision.call(null)").unwrap_err(); + assert!(err.contains("TypeError"), "got: {}", err); +} + +#[test] +fn number_methods_argument_coercion_and_ordering() { + let mut vm = Vm::new(); + // 参数走完整 ToNumber:Symbol/BigInt 抛 TypeError。 + for method in ["toFixed", "toExponential", "toPrecision"] { + let err = eval(&mut vm, &format!("(0).{}(0n)", method)).unwrap_err(); + assert!(err.contains("TypeError"), "bigint for {}: {}", method, err); + let err = eval(&mut vm, &format!("(0).{}(Symbol())", method)).unwrap_err(); + assert!(err.contains("TypeError"), "symbol for {}: {}", method, err); + } + // 参数 valueOf 抛出的原始异常原样传播(保留异常值)。 + let v = eval(&mut vm, "try { (0).toFixed({valueOf(){ throw 42 }}) } catch (e) { e }").unwrap(); + assert_eq!(v.as_int(), 42); + // toFixed:范围检查先于 NaN 短路。 + let err = eval(&mut vm, "NaN.toFixed(Infinity)").unwrap_err(); + assert!(err.contains("RangeError"), "got: {}", err); + assert_eq!(str_of(&mut vm, "NaN.toFixed(0)"), "NaN"); + // toPrecision:NaN/±∞ 专名先于范围检查。 + assert_eq!(str_of(&mut vm, "NaN.toPrecision(1000)"), "NaN"); + assert_eq!(str_of(&mut vm, "(Infinity).toPrecision(1000)"), "Infinity"); + assert_eq!(str_of(&mut vm, "(-Infinity).toPrecision(1000)"), "-Infinity"); + let err = eval(&mut vm, "(10).toPrecision(0)").unwrap_err(); + assert!(err.contains("RangeError"), "got: {}", err); + // toExponential:NaN/±∞ 专名先于范围检查。 + assert_eq!(str_of(&mut vm, "NaN.toExponential(101)"), "NaN"); + assert_eq!(str_of(&mut vm, "(Infinity).toExponential(101)"), "Infinity"); + let err = eval(&mut vm, "(3).toExponential(101)").unwrap_err(); + assert!(err.contains("RangeError"), "got: {}", err); + let err = eval(&mut vm, "(3).toExponential(-1)").unwrap_err(); + assert!(err.contains("RangeError"), "got: {}", err); + // toExponential 的 undefined 走最短式,非按 0 位处理。 + assert_eq!(str_of(&mut vm, "(123.456).toExponential(undefined)"), "1.23456e+2"); +} + +#[test] +fn number_methods_default_precision_and_boundaries() { + let mut vm = Vm::new(); + // toPrecision 缺省 precision = ToString(x)。 + assert_eq!(str_of(&mut vm, "(42).toPrecision()"), "42"); + assert_eq!(str_of(&mut vm, "(123.456).toPrecision(undefined)"), "123.456"); + assert_eq!(str_of(&mut vm, "NaN.toPrecision()"), "NaN"); + assert_eq!(str_of(&mut vm, "Number.prototype.toPrecision()"), "0"); + // toFixed 的 x ≥ 1e21 退化为 String 科学式。 + assert_eq!(str_of(&mut vm, "(1e21).toFixed()"), "1e+21"); + assert_eq!(str_of(&mut vm, "(-1e21).toFixed(0)"), "-1e+21"); + assert_eq!(str_of(&mut vm, "(new Number(1e21)).toFixed()"), "1e+21"); + // -0 三面:构造、toFixed、toExponential。 + assert_eq!(str_of(&mut vm, "String(Object.is(Number(-0), -0))"), "true"); + assert_eq!(str_of(&mut vm, "String(Object.is(new Number(-0).valueOf(), -0))"), "true"); + assert_eq!(str_of(&mut vm, "(-0).toFixed(0)"), "0"); + assert_eq!(str_of(&mut vm, "(-0).toExponential(0)"), "0e+0"); + assert_eq!(str_of(&mut vm, "(-0).toPrecision(2)"), "0.0"); + // 指数恒带符号。 + assert_eq!(str_of(&mut vm, "(3).toExponential(0)"), "3e+0"); + assert_eq!(str_of(&mut vm, "(0).toExponential(1)"), "0.0e+0"); + assert_eq!(str_of(&mut vm, "(10).toPrecision(1)"), "1e+1"); + assert_eq!(str_of(&mut vm, "(1.1e-32).toExponential()"), "1.1e-32"); +} + +#[test] +fn number_isnan_isfinite_strict_type() { + let mut vm = Vm::new(); + // 非 Number 类型(含字符串/对象)一律 false,不强转。 + assert_eq!(str_of(&mut vm, "String(Number.isFinite('1'))"), "false"); + assert_eq!(str_of(&mut vm, "String(Number.isNaN('NaN'))"), "false"); + assert_eq!(str_of(&mut vm, "String(Number.isFinite(new Number(42)))"), "false"); + assert_eq!(str_of(&mut vm, "String(Number.isFinite(42))"), "true"); + assert_eq!(str_of(&mut vm, "String(Number.isNaN(NaN))"), "true"); + assert_eq!(str_of(&mut vm, "String(Number.isFinite(Infinity))"), "false"); +} + +#[test] +fn number_proto_tag_and_metadata() { + let mut vm = Vm::new(); + // Number.prototype 本体 tag 为 Number(品牌表据 type_tag 命中)。 + assert_eq!(str_of(&mut vm, "Object.prototype.toString.call(Number.prototype)"), "[object Number]"); + // parseInt/parseFloat 与全局同名函数是同一函数对象。 + assert_eq!(str_of(&mut vm, "String(Number.parseInt === parseInt)"), "true"); + assert_eq!(str_of(&mut vm, "String(Number.parseFloat === parseFloat)"), "true"); + // toLocaleString 为 own 属性,行为同 toString(locale 参数忽略)。 + assert_eq!(str_of(&mut vm, "String(Number.prototype.hasOwnProperty('toLocaleString'))"), "true"); + assert_eq!(str_of(&mut vm, "(1.5).toLocaleString()"), "1.5"); + // 四方法 length 为 1。 + assert_eq!(str_of(&mut vm, "String(Number.prototype.toString.length)"), "1"); + assert_eq!(str_of(&mut vm, "String(Number.prototype.toFixed.length)"), "1"); + assert_eq!(str_of(&mut vm, "String(Number.prototype.toExponential.length)"), "1"); + assert_eq!(str_of(&mut vm, "String(Number.prototype.toPrecision.length)"), "1"); +} + +/// full_reset 脏重建:number 家族重建后 Number.prototype 仍带 Number 对象 tag +/// 与 +0 包值(漏此分支则 tag 翻回 "Object")。 +#[test] +fn number_proto_tag_survives_dirty_rebuild() { + let mut vm = Vm::new(); + // 属性写使 number 家族置脏,触发选择性重建路径。 + eval(&mut vm, "Number.prototype.marker = 1").unwrap(); + vm.full_reset(); + assert_eq!(str_of(&mut vm, "Object.prototype.toString.call(Number.prototype)"), "[object Number]"); + assert_eq!(str_of(&mut vm, "Number.prototype.toFixed(1)"), "0.0"); + assert_eq!(str_of(&mut vm, "String(Number.parseInt === parseInt)"), "true"); +}