fix: 对齐 Number 原型四方法规范序位并补齐绑定元数据面

This commit is contained in:
2026-09-22 09:59:53 +00:00
parent 4016cfbf9b
commit 550d8f4cba
5 changed files with 367 additions and 67 deletions
+192 -60
View File
@@ -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<i32> {
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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
@@ -55,38 +68,42 @@ pub fn number_constructor<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, this_val: JsValue) -> Result<f64, JsValue> {
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<H: VmHost>(vm: &mut H, arg: JsValue) -> Result<f64, JsValue> {
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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
/// `Number.prototype.toExponential(digits)`:按科学计数法输出;
/// digits 缺省时自动决定小数位数,超范围抛 RangeError。
/// digits 缺省/undefined 取最短精确有效数字数,超范围抛 RangeError。
///
/// 步序:先 thisNumberValue,再转换 fractionDigits(对象参数执行 ToPrimitive
/// 且异常原样传播);NaN/±∞ 专名先于范围检查;指数恒带符号。
pub fn number_to_exponential<H: VmHost>(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<H: VmHost>(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;
@@ -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<JsObject>) {
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<JsObject>) -> 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");
+7 -3
View File
@@ -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())
};
+23 -3
View File
@@ -38,16 +38,23 @@ pub fn bind_number(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
proto,
core,
&[
("toString", oxide_builtins::number::number_to_string::<crate::vm::Vm> as *const (), 0),
("toFixed", oxide_builtins::number::number_to_fixed::<crate::vm::Vm> as *const (), 0),
("toString", oxide_builtins::number::number_to_string::<crate::vm::Vm> as *const (), 1),
("toFixed", oxide_builtins::number::number_to_fixed::<crate::vm::Vm> as *const (), 1),
(
"toExponential",
oxide_builtins::number::number_to_exponential::<crate::vm::Vm> as *const (),
0,
1,
),
(
"toPrecision",
oxide_builtins::number::number_to_precision::<crate::vm::Vm> as *const (),
1,
),
// toLocaleString 为 Number.prototype 的 own 属性(规范 21.7.3.25):
// 保留参数不计 length(length 0),locale 参数忽略、恒十进制。
(
"toLocaleString",
oxide_builtins::number::number_to_locale_string::<crate::vm::Vm> as *const (),
0,
),
("valueOf", oxide_builtins::number::number_value_of::<crate::vm::Vm> as *const (), 0),
@@ -85,4 +92,17 @@ pub fn bind_number(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
("isFinite", oxide_builtins::global::global_is_finite::<crate::vm::Vm> 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);
}
}
}
}
+132 -1
View File
@@ -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");
}