From db8aa7025e537e1ef9c4f3dd6a527d991d6d262e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Sat, 27 Jun 2026 22:46:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Number=20=E9=9D=99=E6=80=81=E6=96=B9?= =?UTF-8?q?=E6=B3=95=20ToNumber=20=E8=BD=AC=E6=8D=A2=E3=80=81=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E6=96=B9=E6=B3=95=E7=B2=BE=E5=BA=A6/?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E4=BF=AE=E5=A4=8D=E3=80=81=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E6=8F=8F=E8=BF=B0=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/number.rs | 123 ++++++++++++------ .../tests/number_methods_tests.rs | 65 +++++++++ crates/oxide_vm/src/bindings/bind_number.rs | 6 +- 3 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 crates/oxide_builtins/tests/number_methods_tests.rs diff --git a/crates/oxide_builtins/src/number.rs b/crates/oxide_builtins/src/number.rs index cdd01d1..ce4232e 100644 --- a/crates/oxide_builtins/src/number.rs +++ b/crates/oxide_builtins/src/number.rs @@ -50,26 +50,15 @@ pub fn number_is_nan(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::bool(false)); } - let val = vm.reg(args[1]); - if val.is_double() { - NativeResult::Ok(JsValue::bool(val.as_double().is_nan())) - } else { - NativeResult::Ok(JsValue::bool(false)) - } + let n = oxide_runtime_api::to_number(vm.reg(args[1])); + NativeResult::Ok(JsValue::bool(n.is_nan())) } pub fn number_is_finite(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::bool(false)); } - let val = vm.reg(args[1]); - if val.is_int() { - return NativeResult::Ok(JsValue::bool(true)); - } - if val.is_double() { - return NativeResult::Ok(JsValue::bool(val.as_double().is_finite())); - } - let n = oxide_runtime_api::to_number(val); + let n = oxide_runtime_api::to_number(vm.reg(args[1])); NativeResult::Ok(JsValue::bool(n.is_finite())) } @@ -184,64 +173,122 @@ pub fn number_to_string(vm: &mut H, args: &[u8]) -> NativeResult { 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 digits = if args.len() > 1 { - vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) 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 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 - } - .min(20); - - let formatted = format!("{:.digits$}", n); + }; + let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n }; + let formatted = format!("{:.precision$}", n_abs, precision = fraction_digits); NativeResult::Ok(vm.new_string(&formatted)) } pub fn number_is_integer(vm: &mut H, args: &[u8]) -> NativeResult { let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); + if !val.is_int() && !val.is_double() { + return NativeResult::Ok(JsValue::bool(false)); + } let n = oxide_runtime_api::to_number(val); NativeResult::Ok(JsValue::bool(n.trunc() == n && n.is_finite())) } pub fn number_is_safe_integer(vm: &mut H, args: &[u8]) -> NativeResult { let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); + if !val.is_int() && !val.is_double() { + return NativeResult::Ok(JsValue::bool(false)); + } let n = oxide_runtime_api::to_number(val); let safe = n.trunc() == n && n.is_finite() && n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64; NativeResult::Ok(JsValue::bool(safe)) } -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 digits = if args.len() > 1 { - vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize - } else { - 0usize +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", + )); } - .min(100); + 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, + "toPrecision() precision must be between 1 and 100", + )); + } + 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 formatted = format!("{:.digits$e}", n); - NativeResult::Ok(vm.new_string(&formatted)) + 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 }; + let formatted = if e >= -6 && e < precision as i32 { + 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) + }; + if is_neg { + NativeResult::Ok(vm.new_string(&format!("-{}", formatted))) + } else { + NativeResult::Ok(vm.new_string(&formatted)) + } } -pub fn number_to_precision(vm: &mut H, args: &[u8]) -> NativeResult { +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 precision = if args.len() > 1 { - vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize - } else { - 0usize - } - .min(21); 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 formatted = format!("{:.precision$}", n); - NativeResult::Ok(vm.new_string(&formatted)) + 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 formatted = format!("{:e}", n_abs); + let mut s = formatted; + if let Some(e_pos) = s.find('e') { + let mut mantissa = s[..e_pos].to_string(); + while mantissa.ends_with('0') && mantissa.len() > 1 { + mantissa.pop(); + } + mantissa = mantissa.trim_end_matches('.').to_string(); + if mantissa.contains('.') { + s = format!("{}{}", mantissa, &s[e_pos..]); + } + } + let result = format!("{}{}", sign_prefix, s); + return NativeResult::Ok(vm.new_string(&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 fraction_digits = raw as usize; + let formatted = format!("{:.digits$e}", n_abs, digits = fraction_digits); + NativeResult::Ok(vm.new_string(&format!("{}{}", sign_prefix, formatted))) } pub fn number_value_of(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_builtins/tests/number_methods_tests.rs b/crates/oxide_builtins/tests/number_methods_tests.rs new file mode 100644 index 0000000..4373382 --- /dev/null +++ b/crates/oxide_builtins/tests/number_methods_tests.rs @@ -0,0 +1,65 @@ +use oxide_runtime_api::to_number; +use oxide_types::value::JsValue; + +#[test] +fn is_nan_nan_returns_true() { + assert!(to_number(JsValue::float(f64::NAN)).is_nan()); +} + +#[test] +fn is_nan_number_123_returns_false() { + assert!(!to_number(JsValue::int(123)).is_nan()); +} + +#[test] +fn is_nan_undefined_returns_true() { + assert!(to_number(JsValue::undefined()).is_nan()); +} + +#[test] +fn is_finite_number_42_returns_true() { + assert!(to_number(JsValue::int(42)).is_finite()); +} + +#[test] +fn is_finite_infinity_returns_false() { + assert!(!to_number(JsValue::float(f64::INFINITY)).is_finite()); +} + +#[test] +fn is_integer_non_number_float_nan() { + let val = JsValue::undefined(); + assert!(!val.is_int() && !val.is_double()); +} + +#[test] +fn is_integer_int_42() { + let val = JsValue::int(42); + assert!(val.is_int()); + let n = to_number(val); + assert!(n.trunc() == n && n.is_finite()); +} + +#[test] +fn is_integer_double_fractional() { + let val = JsValue::float(42.5); + assert!(val.is_double()); + let n = to_number(val); + assert!(!(n.trunc() == n && n.is_finite())); +} + +#[test] +fn is_safe_integer_max_value() { + let val = JsValue::float(9007199254740991f64); + let n = to_number(val); + assert!(n.trunc() == n && n.is_finite()); + assert!(n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64); +} + +#[test] +fn is_safe_integer_overflow() { + let val = JsValue::float(9007199254740992f64); + let n = to_number(val); + assert!(n.trunc() == n && n.is_finite()); + assert!(!(n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64)); +} diff --git a/crates/oxide_vm/src/bindings/bind_number.rs b/crates/oxide_vm/src/bindings/bind_number.rs index e693de5..8597063 100644 --- a/crates/oxide_vm/src/bindings/bind_number.rs +++ b/crates/oxide_vm/src/bindings/bind_number.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut JsObject) { @@ -27,6 +27,8 @@ pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut oxide_builtins::number::number_is_safe_integer:: as *const (), 1, ), + ("parseInt", oxide_builtins::number::number_parse_int:: as *const (), 1), + ("parseFloat", oxide_builtins::number::number_parse_float:: as *const (), 1), ], ); @@ -62,9 +64,11 @@ pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut ("POSITIVE_INFINITY", JsValue::float(f64::INFINITY)), ] { ctor.ensure_hash_props().push(value); + let pos = ctor.ensure_hash_props().len() - 1; let prop_si = core.perm_interner().intern(name).0; let next_shape = core.shape_forge().make_shape(ctor.shape_id(), prop_si); ctor.set_shape_id(next_shape); + ctor.set_data_meta(pos, PropAttributes::new(false, false, false)); } bind_constructor!(core, global, "Number", ctor_ptr, oxide_builtins::number::number_constructor::, 1, hash: true);