Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions core/engine/src/builtins/intl/number_format/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::borrow::Cow;

use boa_gc::{Finalize, Trace};
use fixed_decimal::{Decimal, FloatPrecision, SignDisplay};
use icu_decimal::{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Decimal> {
// 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,
_ => {}
Expand All @@ -856,11 +853,10 @@ pub(crate) fn js_string_to_fixed_decimal(string: &JsString) -> Option<Decimal> {
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()
Expand Down
109 changes: 73 additions & 36 deletions core/engine/src/builtins/number/globals.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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,
string::StaticJsStrings,
};

use boa_macros::js_str;
use cow_utils::CowUtils;
use boa_string::JsStrVariant;

/// Builtin javascript 'isFinite(number)' function.
///
Expand Down Expand Up @@ -154,17 +154,23 @@ fn from_js_str_radix(src: JsStr<'_>, radix: u8) -> Option<f64> {
/// [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<JsValue> {
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),
Expand Down Expand Up @@ -297,40 +303,71 @@ pub(crate) fn parse_float(
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
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::<String>();
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::<f64, _>(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::<f64, _>(s),
JsStrVariant::Utf16(s) => {
let s = String::from_utf16_lossy(s);
fast_float2::parse_partial::<f64, _>(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 {
Expand Down
20 changes: 0 additions & 20 deletions core/engine/src/builtins/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions core/engine/src/value/inner/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
16 changes: 16 additions & 0 deletions core/engine/src/value/inner/nan_boxed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)]
Expand Down
7 changes: 7 additions & 0 deletions core/engine/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading