diff --git a/core/engine/src/builtins/intl/number_format/mod.rs b/core/engine/src/builtins/intl/number_format/mod.rs index b6f1110e3f2..e442923c5c3 100644 --- a/core/engine/src/builtins/intl/number_format/mod.rs +++ b/core/engine/src/builtins/intl/number_format/mod.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use boa_gc::{Finalize, Trace}; use fixed_decimal::{Decimal, FloatPrecision, SignDisplay}; use icu_decimal::{ @@ -30,7 +28,7 @@ use crate::{ NativeFunction, builtins::{ BuiltInConstructor, BuiltInObject, IntrinsicObject, builder::BuiltInBuilder, - options::get_option, string::is_trimmable_whitespace, + options::get_option, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, @@ -825,13 +823,12 @@ fn to_intl_mathematical_value(value: &JsValue, context: &mut Context) -> JsResul pub(crate) fn js_string_to_fixed_decimal(string: &JsString) -> Option { // 1. Let text be ! StringToCodePoints(str). // 2. Let literal be ParseText(text, StringNumericLiteral). - let Ok(string) = string.to_std_string() else { + let Ok(string) = string.trim().to_std_string() else { // 3. If literal is a List of errors, return NaN. return None; }; // 4. Return StringNumericValue of literal. - let string = string.trim_matches(is_trimmable_whitespace); - match string { + match string.as_str() { "" => return Some(Decimal::from(0)), "-Infinity" | "Infinity" | "+Infinity" => return None, _ => {} @@ -856,11 +853,10 @@ pub(crate) fn js_string_to_fixed_decimal(string: &JsString) -> Option { return None; } let int = BigInt::from_str_radix(string, base).ok()?; - let int_str = int.to_string(); - Cow::Owned(int_str) + int.to_string() } else { - Cow::Borrowed(string) + string }; Decimal::try_from_str(&s).ok() diff --git a/core/engine/src/builtins/number/globals.rs b/core/engine/src/builtins/number/globals.rs index 63fc6abdc66..4ef72b4323f 100644 --- a/core/engine/src/builtins/number/globals.rs +++ b/core/engine/src/builtins/number/globals.rs @@ -1,6 +1,6 @@ use crate::{ Context, JsArgs, JsResult, JsStr, JsString, JsValue, - builtins::{BuiltInBuilder, BuiltInObject, IntrinsicObject, string::is_trimmable_whitespace}, + builtins::{BuiltInBuilder, BuiltInObject, IntrinsicObject}, context::intrinsics::Intrinsics, object::JsObject, realm::Realm, @@ -8,7 +8,7 @@ use crate::{ }; use boa_macros::js_str; -use cow_utils::CowUtils; +use boa_string::JsStrVariant; /// Builtin javascript 'isFinite(number)' function. /// @@ -154,17 +154,23 @@ fn from_js_str_radix(src: JsStr<'_>, radix: u8) -> Option { /// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt pub(crate) fn parse_int(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - let (Some(val), radix) = (args.first(), args.get_or_undefined(1)) else { + let (Some(string), radix) = (args.first(), args.get_or_undefined(1)) else { // Not enough arguments to parseInt. return Ok(JsValue::nan()); }; + // OPTIMIZATION: We can skip the round-trip when the value is already a number. + if let Some(int) = string.as_i32() + && radix.is_null_or_undefined() + { + return Ok(JsValue::new(int)); + } + // 1. Let inputString be ? ToString(string). - let input_string = val.to_string(context)?; + let input_string = string.to_string(context)?; // 2. Let S be ! TrimString(inputString, start). let mut s = input_string.trim_start(); - // let mut // 3. Let sign be 1. // 4. If S is not empty and the first code unit of S is the code unit 0x002D (HYPHEN-MINUS), @@ -297,40 +303,72 @@ pub(crate) fn parse_float( args: &[JsValue], context: &mut Context, ) -> JsResult { - if let Some(val) = args.first() { - // TODO: parse float with optimal utf16 algorithm - let input_string = val.to_string(context)?.to_std_string_escaped(); - let s = input_string.trim_start_matches(is_trimmable_whitespace); - let s_prefix = s.chars().take(4).collect::(); - let s_prefix_lower = s_prefix.cow_to_ascii_lowercase(); - // TODO: write our own lexer to match syntax StrDecimalLiteral - if s.starts_with("Infinity") || s.starts_with("+Infinity") { - Ok(JsValue::new(f64::INFINITY)) - } else if s.starts_with("-Infinity") { - Ok(JsValue::new(f64::NEG_INFINITY)) - } else if s_prefix_lower.starts_with("inf") - || s_prefix_lower.starts_with("+inf") - || s_prefix_lower.starts_with("-inf") - { - // Prevent fast_float from parsing "inf", "+inf" as Infinity and "-inf" as -Infinity - Ok(JsValue::nan()) - } else { - Ok(fast_float2::parse_partial::(s).map_or_else( - |_| JsValue::nan(), - |(f, len)| { - if len > 0 { - JsValue::new(f) - } else { - JsValue::nan() - } - }, - )) + const PLUS_CHAR: u16 = b'+' as u16; + const MINUS_CHAR: u16 = b'-' as u16; + const LOWER_CASE_I_CHAR: u16 = b'i' as u16; + const UPPER_CASE_I_CHAR: u16 = b'I' as u16; + + let Some(string) = args.first() else { + return Ok(JsValue::nan()); + }; + + // OPTIMIZATION: We can skip the round-trip when the value is already a number. + if string.is_number() { + // Special case for negative zero - it should become positive zero + if string.is_negative_zero() { + return Ok(JsValue::new(0)); } - } else { - // Not enough arguments to parseFloat. - Ok(JsValue::nan()) + + return Ok(string.clone()); + } + + // 1. Let inputString be ? ToString(string). + let input_string = string.to_string(context)?; + + // 2. Let trimmedString be ! TrimString(inputString, start). + let trimmed_string = input_string.trim_start(); + + // 3. Let trimmed be StringToCodePoints(trimmedString). + // 4. Let trimmedPrefix be the longest prefix of trimmed that satisfies the syntax of a StrDecimalLiteral, which might be trimmed itself. If there is no such prefix, return NaN. + // 5. Let parsedNumber be ParseText(trimmedPrefix, StrDecimalLiteral). + // 6. Assert: parsedNumber is a Parse Node. + // 7. Return the StringNumericValue of parsedNumber. + let (positive, prefix) = match trimmed_string.get(0) { + Some(PLUS_CHAR) => (true, trimmed_string.get(1..).unwrap_or(JsStr::latin1(&[]))), + Some(MINUS_CHAR) => (false, trimmed_string.get(1..).unwrap_or(JsStr::latin1(&[]))), + _ => (true, trimmed_string), + }; + + if prefix.starts_with(js_str!("Infinity")) { + if positive { + return Ok(JsValue::positive_infinity()); + } + return Ok(JsValue::negative_infinity()); + } else if let Some(LOWER_CASE_I_CHAR | UPPER_CASE_I_CHAR) = prefix.get(0) { + return Ok(JsValue::nan()); } + + let value = match trimmed_string.variant() { + JsStrVariant::Latin1(s) => fast_float2::parse_partial::(s), + JsStrVariant::Utf16(s) => { + // TODO: Explore adding direct UTF-16 parsing support to fast_float2. + let s = String::from_utf16_lossy(s); + fast_float2::parse_partial::(s.as_bytes()) + } + }; + + Ok(value.map_or_else( + |_| JsValue::nan(), + |(f, len)| { + if len > 0 { + JsValue::new(f) + } else { + JsValue::nan() + } + }, + )) } + pub(crate) struct ParseFloat; impl IntrinsicObject for ParseFloat { diff --git a/core/engine/src/builtins/string/mod.rs b/core/engine/src/builtins/string/mod.rs index 7f53c7647a2..4b90a4d7651 100644 --- a/core/engine/src/builtins/string/mod.rs +++ b/core/engine/src/builtins/string/mod.rs @@ -57,26 +57,6 @@ pub(crate) enum Placement { End, } -/// Helper function to check if a `char` is trimmable. -pub(crate) const fn is_trimmable_whitespace(c: char) -> bool { - // The rust implementation of `trim` does not regard the same characters whitespace as ecma standard does - // - // Rust uses \p{White_Space} by default, which also includes: - // `\u{0085}' (next line) - // And does not include: - // '\u{FEFF}' (zero width non-breaking space) - // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space - matches!( - c, - '\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' | - // Unicode Space_Separator category - '\u{1680}' | '\u{2000}' - ..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' | - // Line terminators: https://tc39.es/ecma262/#sec-line-terminators - '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}' - ) -} - /// JavaScript `String` implementation. #[derive(Debug, Clone, Copy)] pub(crate) struct String; diff --git a/core/engine/src/value/inner/legacy.rs b/core/engine/src/value/inner/legacy.rs index cd2d69e48db..09986ed1169 100644 --- a/core/engine/src/value/inner/legacy.rs +++ b/core/engine/src/value/inner/legacy.rs @@ -130,6 +130,13 @@ impl EnumBasedValue { matches!(self, Self::Float64(_)) } + /// Returns true if a value is negative zero (`-0`). + #[must_use] + #[inline] + pub(crate) const fn is_negative_zero(&self) -> bool { + matches!(self, Self::Float64(value) if value.to_bits() == (-0f64).to_bits()) + } + /// Returns true if a value is a 32-bits integer. #[must_use] #[inline] diff --git a/core/engine/src/value/inner/nan_boxed.rs b/core/engine/src/value/inner/nan_boxed.rs index 98712fe3289..46c20d36e9d 100644 --- a/core/engine/src/value/inner/nan_boxed.rs +++ b/core/engine/src/value/inner/nan_boxed.rs @@ -184,6 +184,9 @@ mod bits { /// The constant true value. pub(super) const VALUE_TRUE: u64 = MASK_BOOLEAN | 1; + // The constant `-0` value. + pub(super) const VALUE_NEGATIVE_ZERO: u64 = (-0f64).to_bits(); + /// Checks that a value is a valid boolean (either true or false). #[inline(always)] pub(super) const fn is_bool(value: u64) -> bool { @@ -198,6 +201,12 @@ mod bits { || (value & MASK_KIND) == (MASK_NAN | TAG_NAN) } + /// Checks that a value is a negative zero (`-0`). + #[inline(always)] + pub(super) const fn is_negative_zero(value: u64) -> bool { + value == VALUE_NEGATIVE_ZERO + } + /// Checks that a value is a valid integer32. #[inline(always)] pub(super) const fn is_integer32(value: u64) -> bool { @@ -519,6 +528,13 @@ impl NanBoxedValue { bits::is_float(self.value()) } + /// Returns true if a value is negative zero (`-0.0`). + #[must_use] + #[inline(always)] + pub(crate) fn is_negative_zero(&self) -> bool { + bits::is_negative_zero(self.value()) + } + /// Returns true if a value is a 32-bits integer. #[must_use] #[inline(always)] diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs index 153185d1269..344cd8367fd 100644 --- a/core/engine/src/value/mod.rs +++ b/core/engine/src/value/mod.rs @@ -354,6 +354,13 @@ impl JsValue { self.0.is_integer32() || self.0.is_float64() } + /// Returns true if the value is a negative zero (`-0`). + #[inline] + #[must_use] + pub(crate) fn is_negative_zero(&self) -> bool { + self.0.is_negative_zero() + } + /// Returns the number if the value is a number, otherwise `None`. #[inline] #[must_use]