From d4f20406113f31799e443318ef5278264e15e188 Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Thu, 18 Feb 2021 15:46:22 +0100 Subject: [PATCH 1/8] rebase --- boa/src/builtins/math/mod.rs | 703 ----------------- boa/src/builtins/string/mod.rs | 1330 -------------------------------- 2 files changed, 2033 deletions(-) delete mode 100644 boa/src/builtins/math/mod.rs delete mode 100644 boa/src/builtins/string/mod.rs diff --git a/boa/src/builtins/math/mod.rs b/boa/src/builtins/math/mod.rs deleted file mode 100644 index 633b24732f2..00000000000 --- a/boa/src/builtins/math/mod.rs +++ /dev/null @@ -1,703 +0,0 @@ -//! This module implements the global `Math` object. -//! -//! `Math` is a built-in object that has properties and methods for mathematical constants and functions. It’s not a function object. -//! -//! `Math` works with the `Number` type. It doesn't work with `BigInt`. -//! -//! More information: -//! - [ECMAScript reference][spec] -//! - [MDN documentation][mdn] -//! -//! [spec]: https://tc39.es/ecma262/#sec-math-object -//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math - -use crate::{ - builtins::BuiltIn, object::ObjectInitializer, property::Attribute, symbol::WellKnownSymbols, - BoaProfiler, Context, Result, Value, -}; - -#[cfg(test)] -mod tests; - -/// Javascript `Math` object. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct Math; - -impl BuiltIn for Math { - const NAME: &'static str = "Math"; - - fn attribute() -> Attribute { - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE - } - - fn init(context: &mut Context) -> (&'static str, Value, Attribute) { - let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); - use std::f64; - - let to_string_tag = WellKnownSymbols::to_string_tag(); - - let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - let object = ObjectInitializer::new(context) - .property("E", f64::consts::E, attribute) - .property("LN2", f64::consts::LN_2, attribute) - .property("LN10", f64::consts::LN_10, attribute) - .property("LOG2E", f64::consts::LOG2_E, attribute) - .property("LOG10E", f64::consts::LOG10_E, attribute) - .property("SQRT1_2", 0.5_f64.sqrt(), attribute) - .property("SQRT2", f64::consts::SQRT_2, attribute) - .property("PI", f64::consts::PI, attribute) - .property( - to_string_tag, - Self::NAME, - Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .function(Self::abs, "abs", 1) - .function(Self::acos, "acos", 1) - .function(Self::acosh, "acosh", 1) - .function(Self::asin, "asin", 1) - .function(Self::asinh, "asinh", 1) - .function(Self::atan, "atan", 1) - .function(Self::atanh, "atanh", 1) - .function(Self::atan2, "atan2", 2) - .function(Self::cbrt, "cbrt", 1) - .function(Self::ceil, "ceil", 1) - .function(Self::clz32, "clz32", 1) - .function(Self::cos, "cos", 1) - .function(Self::cosh, "cosh", 1) - .function(Self::exp, "exp", 1) - .function(Self::expm1, "expm1", 1) - .function(Self::floor, "floor", 1) - .function(Self::fround, "fround", 1) - .function(Self::hypot, "hypot", 1) - .function(Self::imul, "imul", 1) - .function(Self::log, "log", 1) - .function(Self::log1p, "log1p", 1) - .function(Self::log10, "log10", 1) - .function(Self::log2, "log2", 1) - .function(Self::max, "max", 2) - .function(Self::min, "min", 2) - .function(Self::pow, "pow", 2) - .function(Self::random, "random", 0) - .function(Self::round, "round", 1) - .function(Self::sign, "sign", 1) - .function(Self::sin, "sin", 1) - .function(Self::sinh, "sinh", 1) - .function(Self::sqrt, "sqrt", 1) - .function(Self::tan, "tan", 1) - .function(Self::tanh, "tanh", 1) - .function(Self::trunc, "trunc", 1) - .build(); - - (Self::NAME, object.into(), Self::attribute()) - } -} - -impl Math { - /// Get the absolute value of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.abs - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs - pub(crate) fn abs(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::abs) - .into()) - } - - /// Get the arccos of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.acos - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos - pub(crate) fn acos(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::acos) - .into()) - } - - /// Get the hyperbolic arccos of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.acosh - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh - pub(crate) fn acosh(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::acosh) - .into()) - } - - /// Get the arcsine of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.asin - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin - pub(crate) fn asin(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::asin) - .into()) - } - - /// Get the hyperbolic arcsine of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.asinh - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh - pub(crate) fn asinh(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::asinh) - .into()) - } - - /// Get the arctangent of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.atan - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan - pub(crate) fn atan(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::atan) - .into()) - } - - /// Get the hyperbolic arctangent of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.atanh - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh - pub(crate) fn atanh(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::atanh) - .into()) - } - - /// Get the arctangent of a numbers. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.atan2 - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 - pub(crate) fn atan2(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(match ( - args.get(0).map(|x| x.to_number(context)).transpose()?, - args.get(1).map(|x| x.to_number(context)).transpose()?, - ) { - (Some(x), Some(y)) => x.atan2(y), - (_, _) => f64::NAN, - } - .into()) - } - - /// Get the cubic root of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.cbrt - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt - pub(crate) fn cbrt(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::cbrt) - .into()) - } - - /// Get lowest integer above a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.ceil - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil - pub(crate) fn ceil(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::ceil) - .into()) - } - - /// Get the number of leading zeros in the 32 bit representation of a number - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.clz32 - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 - pub(crate) fn clz32(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_u32(context)) - .transpose()? - .map(u32::leading_zeros) - .unwrap_or(32) - .into()) - } - - /// Get the cosine of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.cos - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos - pub(crate) fn cos(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::cos) - .into()) - } - - /// Get the hyperbolic cosine of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.cosh - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh - pub(crate) fn cosh(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::cosh) - .into()) - } - - /// Get the power to raise the natural logarithm to get the number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.exp - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp - pub(crate) fn exp(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::exp) - .into()) - } - - /// The Math.expm1() function returns e^x - 1, where x is the argument, and e the base of - /// the natural logarithms. The result is computed in a way that is accurate even when the - /// value of x is close 0 - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.expm1 - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1 - pub(crate) fn expm1(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::exp_m1) - .into()) - } - - /// Get the highest integer below a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.floor - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor - pub(crate) fn floor(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::floor) - .into()) - } - - /// Get the nearest 32-bit single precision float representation of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.fround - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround - pub(crate) fn fround(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, |x| (x as f32) as f64) - .into()) - } - - /// Get an approximation of the square root of the sum of squares of all arguments. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.hypot - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot - pub(crate) fn hypot(_: &Value, args: &[Value], context: &mut Context) -> Result { - let mut result = 0f64; - for arg in args { - let x = arg.to_number(context)?; - result = result.hypot(x); - } - Ok(result.into()) - } - - /// Get the result of the C-like 32-bit multiplication of the two parameters. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.imul - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul - pub(crate) fn imul(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(match ( - args.get(0).map(|x| x.to_u32(context)).transpose()?, - args.get(1).map(|x| x.to_u32(context)).transpose()?, - ) { - (Some(x), Some(y)) => x.wrapping_mul(y) as i32, - (_, _) => 0, - } - .into()) - } - - /// Get the natural logarithm of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.log - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log - pub(crate) fn log(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, |x| if x <= 0.0 { f64::NAN } else { x.ln() }) - .into()) - } - - /// Get approximation to the natural logarithm of 1 + x. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.log1p - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p - pub(crate) fn log1p(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::ln_1p) - .into()) - } - - /// Get the base 10 logarithm of the number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.log10 - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10 - pub(crate) fn log10(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, |x| if x <= 0.0 { f64::NAN } else { x.log10() }) - .into()) - } - - /// Get the base 2 logarithm of the number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.log2 - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2 - pub(crate) fn log2(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, |x| if x <= 0.0 { f64::NAN } else { x.log2() }) - .into()) - } - - /// Get the maximum of several numbers. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.max - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max - pub(crate) fn max(_: &Value, args: &[Value], context: &mut Context) -> Result { - let mut max = f64::NEG_INFINITY; - for arg in args { - let num = arg.to_number(context)?; - max = max.max(num); - } - Ok(max.into()) - } - - /// Get the minimum of several numbers. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.min - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min - pub(crate) fn min(_: &Value, args: &[Value], context: &mut Context) -> Result { - let mut min = f64::INFINITY; - for arg in args { - let num = arg.to_number(context)?; - min = min.min(num); - } - Ok(min.into()) - } - - /// Raise a number to a power. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.pow - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow - pub(crate) fn pow(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(match ( - args.get(0).map(|x| x.to_number(context)).transpose()?, - args.get(1).map(|x| x.to_number(context)).transpose()?, - ) { - (Some(x), Some(y)) => x.powf(y), - (_, _) => f64::NAN, - } - .into()) - } - - /// Generate a random floating-point number between `0` and `1`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.random - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random - pub(crate) fn random(_: &Value, _: &[Value], _: &mut Context) -> Result { - Ok(rand::random::().into()) - } - - /// Round a number to the nearest integer. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.round - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round - pub(crate) fn round(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::round) - .into()) - } - - /// Get the sign of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.sign - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign - pub(crate) fn sign(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or( - f64::NAN, - |x| { - if x == 0.0 || x == -0.0 { - x - } else { - x.signum() - } - }, - ) - .into()) - } - - /// Get the sine of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.sin - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin - pub(crate) fn sin(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::sin) - .into()) - } - - /// Get the hyperbolic sine of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.sinh - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh - pub(crate) fn sinh(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::sinh) - .into()) - } - - /// Get the square root of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.sqrt - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt - pub(crate) fn sqrt(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::sqrt) - .into()) - } - - /// Get the tangent of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.tan - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan - pub(crate) fn tan(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::tan) - .into()) - } - - /// Get the hyperbolic tangent of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.tanh - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh - pub(crate) fn tanh(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::tanh) - .into()) - } - - /// Get the integer part of a number. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-math.trunc - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc - pub(crate) fn trunc(_: &Value, args: &[Value], context: &mut Context) -> Result { - Ok(args - .get(0) - .map(|x| x.to_number(context)) - .transpose()? - .map_or(f64::NAN, f64::trunc) - .into()) - } -} diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs deleted file mode 100644 index 3b6c15271af..00000000000 --- a/boa/src/builtins/string/mod.rs +++ /dev/null @@ -1,1330 +0,0 @@ -//! This module implements the global `String` object. -//! -//! The `String` global object is a constructor for strings or a sequence of characters. -//! -//! More information: -//! - [ECMAScript reference][spec] -//! - [MDN documentation][mdn] -//! -//! [spec]: https://tc39.es/ecma262/#sec-string-object -//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String - -pub mod string_iterator; -#[cfg(test)] -mod tests; - -use crate::builtins::Symbol; -use crate::object::PROTOTYPE; -use crate::property::DataDescriptor; -use crate::{ - builtins::{string::string_iterator::StringIterator, Array, BuiltIn, RegExp}, - object::{ConstructorBuilder, Object, ObjectData}, - property::Attribute, - symbol::WellKnownSymbols, - value::{RcString, Value}, - BoaProfiler, Context, Result, -}; -use regress::Regex; -use std::{ - char::{decode_utf16, from_u32}, - cmp::{max, min}, - string::String as StdString, -}; - -pub(crate) fn code_point_at(string: RcString, position: i32) -> Option<(u32, u8, bool)> { - let size = string.encode_utf16().count() as i32; - if position < 0 || position >= size { - return None; - } - let mut encoded = string.encode_utf16(); - let first = encoded.nth(position as usize)?; - if !is_leading_surrogate(first) && !is_trailing_surrogate(first) { - return Some((first as u32, 1, false)); - } - if is_trailing_surrogate(first) || position + 1 == size { - return Some((first as u32, 1, true)); - } - let second = encoded.next()?; - if !is_trailing_surrogate(second) { - return Some((first as u32, 1, true)); - } - let cp = (first as u32 - 0xD800) * 0x400 + (second as u32 - 0xDC00) + 0x10000; - Some((cp, 2, false)) -} - -/// Helper function to check if a `char` is trimmable. -#[inline] -pub(crate) 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}' - ) -} - -fn is_leading_surrogate(value: u16) -> bool { - (0xD800..=0xDBFF).contains(&value) -} - -fn is_trailing_surrogate(value: u16) -> bool { - (0xDC00..=0xDFFF).contains(&value) -} - -/// JavaScript `String` implementation. -#[derive(Debug, Clone, Copy)] -pub(crate) struct String; - -impl BuiltIn for String { - const NAME: &'static str = "String"; - - fn attribute() -> Attribute { - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE - } - - fn init(context: &mut Context) -> (&'static str, Value, Attribute) { - let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); - - let symbol_iterator = WellKnownSymbols::iterator(); - - let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - let string_object = ConstructorBuilder::with_standard_object( - context, - Self::constructor, - context.standard_objects().string_object().clone(), - ) - .name(Self::NAME) - .length(Self::LENGTH) - .property("length", 0, attribute) - .method(Self::char_at, "charAt", 1) - .method(Self::char_code_at, "charCodeAt", 1) - .method(Self::code_point_at, "codePointAt", 1) - .method(Self::to_string, "toString", 0) - .method(Self::concat, "concat", 1) - .method(Self::repeat, "repeat", 1) - .method(Self::slice, "slice", 2) - .method(Self::starts_with, "startsWith", 1) - .method(Self::ends_with, "endsWith", 1) - .method(Self::includes, "includes", 1) - .method(Self::index_of, "indexOf", 1) - .method(Self::last_index_of, "lastIndexOf", 1) - .method(Self::r#match, "match", 1) - .method(Self::pad_end, "padEnd", 1) - .method(Self::pad_start, "padStart", 1) - .method(Self::trim, "trim", 0) - .method(Self::trim_start, "trimStart", 0) - .method(Self::trim_end, "trimEnd", 0) - .method(Self::to_lowercase, "toLowerCase", 0) - .method(Self::to_uppercase, "toUpperCase", 0) - .method(Self::substring, "substring", 2) - .method(Self::substr, "substr", 2) - .method(Self::split, "split", 2) - .method(Self::value_of, "valueOf", 0) - .method(Self::match_all, "matchAll", 1) - .method(Self::replace, "replace", 2) - .method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0) - .build(); - - (Self::NAME, string_object.into(), Self::attribute()) - } -} - -impl String { - /// The amount of arguments this function object takes. - pub(crate) const LENGTH: usize = 1; - - /// JavaScript strings must be between `0` and less than positive `Infinity` and cannot be a negative number. - /// The range of allowed values can be described like this: `[0, +∞)`. - /// - /// The resulting string can also not be larger than the maximum string size, - /// which can differ in JavaScript engines. In Boa it is `2^32 - 1` - pub(crate) const MAX_STRING_LENGTH: f64 = u32::MAX as f64; - - /// `String( value )` - /// - /// - pub(crate) fn constructor( - new_target: &Value, - args: &[Value], - context: &mut Context, - ) -> Result { - // This value is used by console.log and other routines to match Object type - // to its Javascript Identifier (global constructor method name) - let string = match args.get(0) { - Some(value) if value.is_symbol() && new_target.is_undefined() => { - Symbol::to_string(value, &[], context)? - .as_string() - .expect("'Symbol::to_string' returns 'Value::String'") - .clone() - } - Some(ref value) => value.to_string(context)?, - None => RcString::default(), - }; - - if new_target.is_undefined() { - return Ok(string.into()); - } - let prototype = new_target - .as_object() - .and_then(|obj| { - obj.get(&PROTOTYPE.into(), obj.clone().into(), context) - .map(|o| o.as_object()) - .transpose() - }) - .transpose()? - .unwrap_or_else(|| context.standard_objects().object_object().prototype()); - let this = Value::new_object(context); - - this.as_object() - .expect("this should be an object") - .set_prototype_instance(prototype.into()); - - let length = DataDescriptor::new( - Value::from(string.encode_utf16().count()), - Attribute::NON_ENUMERABLE, - ); - this.set_property("length", length); - - this.set_data(ObjectData::String(string)); - - Ok(this) - } - - fn this_string_value(this: &Value, context: &mut Context) -> Result { - match this { - Value::String(ref string) => return Ok(string.clone()), - Value::Object(ref object) => { - let object = object.borrow(); - if let Some(string) = object.as_string() { - return Ok(string); - } - } - _ => {} - } - - Err(context.construct_type_error("'this' is not a string")) - } - - /// Get the string value to a primitive string - #[allow(clippy::wrong_self_convention)] - #[inline] - pub(crate) fn to_string(this: &Value, _: &[Value], context: &mut Context) -> Result { - // Get String from String Object and send it back as a new value - Ok(Value::from(Self::this_string_value(this, context)?)) - } - - /// `String.prototype.charAt( index )` - /// - /// The `String` object's `charAt()` method returns a new string consisting of the single UTF-16 code unit located at the specified offset into the string. - /// - /// Characters in a string are indexed from left to right. The index of the first character is `0`, - /// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`. - /// If the `index` you supply is out of this range, JavaScript returns an empty string. - /// - /// If no index is provided to `charAt()`, the default is `0`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt - pub(crate) fn char_at(this: &Value, args: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - let pos = args - .get(0) - .cloned() - .unwrap_or_else(Value::undefined) - .to_integer(context)? as i32; - - // Fast path returning empty string when pos is obviously out of range - if pos < 0 || pos >= primitive_val.len() as i32 { - return Ok("".into()); - } - - // Calling .len() on a string would give the wrong result, as they are bytes not the number of - // unicode code points - // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of - // bytes is an O(1) operation. - if let Some(utf16_val) = primitive_val.encode_utf16().nth(pos as usize) { - Ok(Value::from(from_u32(utf16_val as u32).unwrap())) - } else { - Ok("".into()) - } - } - - /// `String.prototype.codePointAt( index )` - /// - /// The `codePointAt()` method returns an integer between `0` to `1114111` (`0x10FFFF`) representing the UTF-16 code unit at the given index. - /// - /// If no UTF-16 surrogate pair begins at the index, the code point at the index is returned. - /// - /// `codePointAt()` returns `undefined` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.codepointat - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt - pub(crate) fn code_point_at( - this: &Value, - args: &[Value], - context: &mut Context, - ) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - let pos = args - .get(0) - .cloned() - .unwrap_or_else(Value::undefined) - .to_integer(context)? as i32; - - // Fast path returning undefined when pos is obviously out of range - if pos < 0 || pos >= primitive_val.len() as i32 { - return Ok(Value::undefined()); - } - - if let Some((code_point, _, _)) = code_point_at(primitive_val, pos) { - Ok(Value::from(code_point)) - } else { - Ok(Value::undefined()) - } - } - - /// `String.prototype.charCodeAt( index )` - /// - /// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index. - /// - /// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding. - /// - /// `charCodeAt()` returns `NaN` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt - pub(crate) fn char_code_at( - this: &Value, - args: &[Value], - context: &mut Context, - ) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - let pos = args - .get(0) - .cloned() - .unwrap_or_else(Value::undefined) - .to_integer(context)? as i32; - - // Fast path returning NaN when pos is obviously out of range - if pos < 0 || pos >= primitive_val.len() as i32 { - return Ok(Value::nan()); - } - - // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points - // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. - // If there is no element at that index, the result is NaN - if let Some(utf16_val) = primitive_val.encode_utf16().nth(pos as usize) { - Ok(Value::from(f64::from(utf16_val))) - } else { - Ok(Value::nan()) - } - } - - /// `String.prototype.concat( str1[, ...strN] )` - /// - /// The `concat()` method concatenates the string arguments to the calling string and returns a new string. - /// - /// Changes to the original string or the returned string don't affect the other. - /// - /// If the arguments are not of the type string, they are converted to string values before concatenating. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat - pub(crate) fn concat(this: &Value, args: &[Value], context: &mut Context) -> Result { - let object = this.require_object_coercible(context)?; - let mut string = object.to_string(context)?.to_string(); - - for arg in args { - string.push_str(&arg.to_string(context)?); - } - - Ok(Value::from(string)) - } - - /// `String.prototype.repeat( count )` - /// - /// The `repeat()` method constructs and returns a new string which contains the specified number of - /// copies of the string on which it was called, concatenated together. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat - pub(crate) fn repeat(this: &Value, args: &[Value], context: &mut Context) -> Result { - let object = this.require_object_coercible(context)?; - let string = object.to_string(context)?; - - if let Some(arg) = args.get(0) { - let n = arg.to_integer(context)?; - if n < 0.0 { - return context.throw_range_error("repeat count cannot be a negative number"); - } - - if n.is_infinite() { - return context.throw_range_error("repeat count cannot be infinity"); - } - - if n * (string.len() as f64) > Self::MAX_STRING_LENGTH { - return context - .throw_range_error("repeat count must not overflow maximum string length"); - } - Ok(string.repeat(n as usize).into()) - } else { - Ok("".into()) - } - } - - /// `String.prototype.slice( beginIndex [, endIndex] )` - /// - /// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice - pub(crate) fn slice(this: &Value, args: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - - // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points - // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. - let length = primitive_val.chars().count() as i32; - - let start = args - .get(0) - .cloned() - .unwrap_or_else(Value::undefined) - .to_integer(context)? as i32; - let end = args - .get(1) - .cloned() - .unwrap_or_else(|| Value::integer(length)) - .to_integer(context)? as i32; - - let from = if start < 0 { - max(length.wrapping_add(start), 0) - } else { - min(start, length) - }; - let to = if end < 0 { - max(length.wrapping_add(end), 0) - } else { - min(end, length) - }; - - let span = max(to.wrapping_sub(from), 0); - - let new_str: StdString = primitive_val - .chars() - .skip(from as usize) - .take(span as usize) - .collect(); - Ok(Value::from(new_str)) - } - - /// `String.prototype.startWith( searchString[, position] )` - /// - /// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith - pub(crate) fn starts_with( - this: &Value, - args: &[Value], - context: &mut Context, - ) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - - let arg = args.get(0).cloned().unwrap_or_else(Value::undefined); - - if Self::is_regexp_object(&arg) { - context.throw_type_error( - "First argument to String.prototype.startsWith must not be a regular expression", - )?; - } - - let search_string = arg.to_string(context)?; - - let length = primitive_val.chars().count() as i32; - let search_length = search_string.chars().count() as i32; - - // If less than 2 args specified, position is 'undefined', defaults to 0 - let position = if args.len() < 2 { - 0 - } else { - args.get(1) - .expect("failed to get arg") - .to_integer(context)? as i32 - }; - - let start = min(max(position, 0), length); - let end = start.wrapping_add(search_length); - - if end > length { - Ok(Value::from(false)) - } else { - // Only use the part of the string from "start" - let this_string: StdString = primitive_val.chars().skip(start as usize).collect(); - Ok(Value::from(this_string.starts_with(search_string.as_str()))) - } - } - - /// `String.prototype.endsWith( searchString[, length] )` - /// - /// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith - pub(crate) fn ends_with(this: &Value, args: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - - let arg = args.get(0).cloned().unwrap_or_else(Value::undefined); - - if Self::is_regexp_object(&arg) { - context.throw_type_error( - "First argument to String.prototype.endsWith must not be a regular expression", - )?; - } - - let search_string = arg.to_string(context)?; - - let length = primitive_val.chars().count() as i32; - let search_length = search_string.chars().count() as i32; - - // If less than 2 args specified, end_position is 'undefined', defaults to - // length of this - let end_position = if args.len() < 2 { - length - } else { - args.get(1) - .expect("Could not get argumetn") - .to_integer(context)? as i32 - }; - - let end = min(max(end_position, 0), length); - let start = end.wrapping_sub(search_length); - - if start < 0 { - Ok(Value::from(false)) - } else { - // Only use the part of the string up to "end" - let this_string: StdString = primitive_val.chars().take(end as usize).collect(); - Ok(Value::from(this_string.ends_with(search_string.as_str()))) - } - } - - /// `String.prototype.includes( searchString[, position] )` - /// - /// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes - pub(crate) fn includes(this: &Value, args: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - - let arg = args.get(0).cloned().unwrap_or_else(Value::undefined); - - if Self::is_regexp_object(&arg) { - context.throw_type_error( - "First argument to String.prototype.includes must not be a regular expression", - )?; - } - - let search_string = arg.to_string(context)?; - - let length = primitive_val.chars().count() as i32; - - // If less than 2 args specified, position is 'undefined', defaults to 0 - let position = if args.len() < 2 { - 0 - } else { - args.get(1) - .expect("Could not get argument") - .to_integer(context)? as i32 - }; - - let start = min(max(position, 0), length); - - // Take the string from "this" and use only the part of it after "start" - let this_string: StdString = primitive_val.chars().skip(start as usize).collect(); - - Ok(Value::from(this_string.contains(search_string.as_str()))) - } - - /// Return either the string itself or the string of the regex equivalent - fn get_regex_string(value: &Value) -> StdString { - match value { - Value::String(ref body) => body.to_string(), - Value::Object(ref obj) => { - let obj = obj.borrow(); - - if let Some(regexp) = obj.as_regexp() { - // first argument is another `RegExp` object, so copy its pattern and flags - return regexp.original_source.clone().into(); - } - "undefined".to_string() - } - _ => "undefined".to_string(), - } - } - - fn is_regexp_object(value: &Value) -> bool { - match value { - Value::Object(ref obj) => obj.borrow().is_regexp(), - _ => false, - } - } - - /// `String.prototype.replace( regexp|substr, newSubstr|function )` - /// - /// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`. - /// - /// The `pattern` can be a string or a `RegExp`, and the `replacement` can be a string or a function to be called for each match. - /// If `pattern` is a string, only the first occurrence will be replaced. - /// - /// The original string is left unchanged. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace - pub(crate) fn replace(this: &Value, args: &[Value], context: &mut Context) -> Result { - // TODO: Support Symbol replacer - let primitive_val = this.to_string(context)?; - if args.is_empty() { - return Ok(Value::from(primitive_val)); - } - - let regex_body = Self::get_regex_string(args.get(0).expect("Value needed")); - let re = Regex::new(®ex_body).expect("unable to convert regex to regex object"); - let mat = match re.find(&primitive_val) { - Some(mat) => mat, - None => return Ok(Value::from(primitive_val)), - }; - let caps = re - .find(&primitive_val) - .expect("unable to get capture groups from text") - .captures; - - let replace_value = if args.len() > 1 { - // replace_object could be a string or function or not exist at all - let replace_object: &Value = args.get(1).expect("second argument expected"); - match replace_object { - Value::String(val) => { - // https://tc39.es/ecma262/#table-45 - let mut result = StdString::new(); - let mut chars = val.chars().peekable(); - - let m = caps.len(); - - while let Some(first) = chars.next() { - if first == '$' { - let second = chars.next(); - let second_is_digit = second.map_or(false, |ch| ch.is_digit(10)); - // we use peek so that it is still in the iterator if not used - let third = if second_is_digit { chars.peek() } else { None }; - let third_is_digit = third.map_or(false, |ch| ch.is_digit(10)); - - match (second, third) { - (Some('$'), _) => { - // $$ - result.push('$'); - } - (Some('&'), _) => { - // $& - result.push_str(&primitive_val[mat.range()]); - } - (Some('`'), _) => { - // $` - let start_of_match = mat.start(); - result.push_str(&primitive_val[..start_of_match]); - } - (Some('\''), _) => { - // $' - let end_of_match = mat.end(); - result.push_str(&primitive_val[end_of_match..]); - } - (Some(second), Some(third)) - if second_is_digit && third_is_digit => - { - // $nn - let tens = second.to_digit(10).unwrap() as usize; - let units = third.to_digit(10).unwrap() as usize; - let nn = 10 * tens + units; - if nn == 0 || nn > m { - result.push(first); - result.push(second); - if let Some(ch) = chars.next() { - result.push(ch); - } - } else { - let group = match mat.group(nn) { - Some(range) => &primitive_val[range.clone()], - _ => "", - }; - result.push_str(group); - chars.next(); // consume third - } - } - (Some(second), _) if second_is_digit => { - // $n - let n = second.to_digit(10).unwrap() as usize; - if n == 0 || n > m { - result.push(first); - result.push(second); - } else { - let group = match mat.group(n) { - Some(range) => &primitive_val[range.clone()], - _ => "", - }; - result.push_str(group); - } - } - (Some('<'), _) => { - // $< - // TODO: named capture groups - result.push_str("$<"); - } - _ => { - // $?, ? is none of the above - // we can consume second because it isn't $ - result.push(first); - if let Some(second) = second { - result.push(second); - } - } - } - } else { - result.push(first); - } - } - - result - } - Value::Object(_) => { - // This will return the matched substring first, then captured parenthesized groups later - let mut results: Vec = mat - .groups() - .map(|group| match group { - Some(range) => Value::from(&primitive_val[range]), - None => Value::undefined(), - }) - .collect(); - - // Returns the starting byte offset of the match - let start = mat.start(); - results.push(Value::from(start)); - // Push the whole string being examined - results.push(Value::from(primitive_val.to_string())); - - let result = context.call(&replace_object, this, &results)?; - - result.to_string(context)?.to_string() - } - _ => "undefined".to_string(), - } - } else { - "undefined".to_string() - }; - - Ok(Value::from(primitive_val.replacen( - &primitive_val[mat.range()], - &replace_value, - 1, - ))) - } - - /// `String.prototype.indexOf( searchValue[, fromIndex] )` - /// - /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence - /// of the specified value, starting the search at `fromIndex`. - /// - /// Returns `-1` if the value is not found. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf - pub(crate) fn index_of(this: &Value, args: &[Value], context: &mut Context) -> Result { - let this = this.require_object_coercible(context)?; - let string = this.to_string(context)?; - - let search_string = args - .get(0) - .cloned() - .unwrap_or_else(Value::undefined) - .to_string(context)?; - - let length = string.chars().count(); - let start = args - .get(1) - .map(|position| position.to_integer(context)) - .transpose()? - .map_or(0, |position| position.max(0.0).min(length as f64) as usize); - - if search_string.is_empty() { - return Ok(start.min(length).into()); - } - - if start < length { - if let Some(position) = string.find(search_string.as_str()) { - return Ok(string[..position].chars().count().into()); - } - } - - Ok(Value::from(-1)) - } - - /// `String.prototype.lastIndexOf( searchValue[, fromIndex] )` - /// - /// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence - /// of the specified value, searching backwards from `fromIndex`. - /// - /// Returns `-1` if the value is not found. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf - pub(crate) fn last_index_of( - this: &Value, - args: &[Value], - context: &mut Context, - ) -> Result { - let this = this.require_object_coercible(context)?; - let string = this.to_string(context)?; - - let search_string = args - .get(0) - .cloned() - .unwrap_or_else(Value::undefined) - .to_string(context)?; - - let length = string.chars().count(); - let start = args - .get(1) - .map(|position| position.to_integer(context)) - .transpose()? - .map_or(0, |position| position.max(0.0).min(length as f64) as usize); - - if search_string.is_empty() { - return Ok(start.min(length).into()); - } - - if start < length { - if let Some(position) = string.rfind(search_string.as_str()) { - return Ok(string[..position].chars().count().into()); - } - } - - Ok(Value::from(-1)) - } - - /// `String.prototype.match( regexp )` - /// - /// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex]. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match - /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - pub(crate) fn r#match(this: &Value, args: &[Value], context: &mut Context) -> Result { - let re = RegExp::constructor( - &Value::from(Object::default()), - &[args.get(0).cloned().unwrap_or_default()], - context, - )?; - RegExp::r#match(&re, this.to_string(context)?, context) - } - - /// Abstract method `StringPad`. - /// - /// Performs the actual string padding for padStart/End. - /// - fn string_pad( - primitive: RcString, - max_length: i32, - fill_string: Option, - at_start: bool, - ) -> Value { - let primitive_length = primitive.len() as i32; - - if max_length <= primitive_length { - return Value::from(primitive); - } - - let filter = fill_string.as_deref().unwrap_or(" "); - - let fill_len = max_length.wrapping_sub(primitive_length); - let mut fill_str = StdString::new(); - - while fill_str.len() < fill_len as usize { - fill_str.push_str(filter); - } - // Cut to size max_length - let concat_fill_str: StdString = fill_str.chars().take(fill_len as usize).collect(); - - if at_start { - Value::from(format!("{}{}", concat_fill_str, &primitive)) - } else { - Value::from(format!("{}{}", primitive, &concat_fill_str)) - } - } - - /// `String.prototype.padEnd( targetLength[, padString] )` - /// - /// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length. - /// - /// The padding is applied from the end of the current string. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd - pub(crate) fn pad_end(this: &Value, args: &[Value], context: &mut Context) -> Result { - let primitive = this.to_string(context)?; - if args.is_empty() { - return Err(Value::from("padEnd requires maxLength argument")); - } - let max_length = args - .get(0) - .expect("failed to get argument for String method") - .to_integer(context)? as i32; - - let fill_string = args.get(1).map(|arg| arg.to_string(context)).transpose()?; - - Ok(Self::string_pad(primitive, max_length, fill_string, false)) - } - - /// `String.prototype.padStart( targetLength [, padString] )` - /// - /// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length. - /// - /// The padding is applied from the start of the current string. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart - pub(crate) fn pad_start(this: &Value, args: &[Value], context: &mut Context) -> Result { - let primitive = this.to_string(context)?; - if args.is_empty() { - return Err(Value::from("padStart requires maxLength argument")); - } - let max_length = args - .get(0) - .expect("failed to get argument for String method") - .to_integer(context)? as i32; - - let fill_string = args.get(1).map(|arg| arg.to_string(context)).transpose()?; - - Ok(Self::string_pad(primitive, max_length, fill_string, true)) - } - - /// String.prototype.trim() - /// - /// The `trim()` method removes whitespace from both ends of a string. - /// - /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim - pub(crate) fn trim(this: &Value, _: &[Value], context: &mut Context) -> Result { - let this = this.require_object_coercible(context)?; - let string = this.to_string(context)?; - Ok(Value::from(string.trim_matches(is_trimmable_whitespace))) - } - - /// `String.prototype.trimStart()` - /// - /// The `trimStart()` method removes whitespace from the beginning of a string. - /// - /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart - pub(crate) fn trim_start(this: &Value, _: &[Value], context: &mut Context) -> Result { - let string = this.to_string(context)?; - Ok(Value::from( - string.trim_start_matches(is_trimmable_whitespace), - )) - } - - /// String.prototype.trimEnd() - /// - /// The `trimEnd()` method removes whitespace from the end of a string. - /// - /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd - pub(crate) fn trim_end(this: &Value, _: &[Value], context: &mut Context) -> Result { - let this = this.require_object_coercible(context)?; - let string = this.to_string(context)?; - Ok(Value::from( - string.trim_end_matches(is_trimmable_whitespace), - )) - } - - /// `String.prototype.toLowerCase()` - /// - /// The `toLowerCase()` method returns the calling string value converted to lower case. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tolowercase - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase - #[allow(clippy::wrong_self_convention)] - pub(crate) fn to_lowercase(this: &Value, _: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let this_str = this.to_string(context)?; - // The Rust String is mapped to uppercase using the builtin .to_lowercase(). - // There might be corner cases where it does not behave exactly like Javascript expects - Ok(Value::from(this_str.to_lowercase())) - } - - /// `String.prototype.toUpperCase()` - /// - /// The `toUpperCase()` method returns the calling string value converted to uppercase. - /// - /// The value will be **converted** to a string if it isn't one - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase - #[allow(clippy::wrong_self_convention)] - pub(crate) fn to_uppercase(this: &Value, _: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let this_str = this.to_string(context)?; - // The Rust String is mapped to uppercase using the builtin .to_uppercase(). - // There might be corner cases where it does not behave exactly like Javascript expects - Ok(Value::from(this_str.to_uppercase())) - } - - /// `String.prototype.substring( indexStart[, indexEnd] )` - /// - /// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring - pub(crate) fn substring(this: &Value, args: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - // If no args are specified, start is 'undefined', defaults to 0 - let start = if args.is_empty() { - 0 - } else { - args.get(0) - .expect("failed to get argument for String method") - .to_integer(context)? as i32 - }; - let length = primitive_val.encode_utf16().count() as i32; - // If less than 2 args specified, end is the length of the this object converted to a String - let end = if args.len() < 2 { - length - } else { - args.get(1) - .expect("Could not get argument") - .to_integer(context)? as i32 - }; - // Both start and end args replaced by 0 if they were negative - // or by the length of the String if they were greater - let final_start = min(max(start, 0), length); - let final_end = min(max(end, 0), length); - // Start and end are swapped if start is greater than end - let from = min(final_start, final_end) as usize; - let to = max(final_start, final_end) as usize; - // Extract the part of the string contained between the start index and the end index - // where start is guaranteed to be smaller or equals to end - let extracted_string: std::result::Result = decode_utf16( - primitive_val - .encode_utf16() - .skip(from) - .take(to.wrapping_sub(from)), - ) - .collect(); - Ok(Value::from(extracted_string.expect("Invalid string"))) - } - - /// `String.prototype.substr( start[, length] )` - /// - /// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr - /// - pub(crate) fn substr(this: &Value, args: &[Value], context: &mut Context) -> Result { - // First we get it the actual string a private field stored on the object only the context has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = this.to_string(context)?; - // If no args are specified, start is 'undefined', defaults to 0 - let mut start = if args.is_empty() { - 0 - } else { - args.get(0) - .expect("failed to get argument for String method") - .to_integer(context)? as i32 - }; - let length = primitive_val.chars().count() as i32; - // If less than 2 args specified, end is +infinity, the maximum number value. - // Using i32::max_value() should be safe because the final length used is at most - // the number of code units from start to the end of the string, - // which should always be smaller or equals to both +infinity and i32::max_value - let end = if args.len() < 2 { - i32::MAX - } else { - args.get(1) - .expect("Could not get argument") - .to_integer(context)? as i32 - }; - // If start is negative it become the number of code units from the end of the string - if start < 0 { - start = max(length.wrapping_add(start), 0); - } - // length replaced by 0 if it was negative - // or by the number of code units from start to the end of the string if it was greater - let result_length = min(max(end, 0), length.wrapping_sub(start)); - // If length is negative we return an empty string - // otherwise we extract the part of the string from start and is length code units long - if result_length <= 0 { - Ok(Value::from("")) - } else { - let extracted_string: StdString = primitive_val - .chars() - .skip(start as usize) - .take(result_length as usize) - .collect(); - - Ok(Value::from(extracted_string)) - } - } - - /// String.prototype.split() - /// - /// The `split()` method divides a String into an ordered list of substrings, puts these substrings into an array, and returns the array. - /// - /// The division is done by searching for a pattern; where the pattern is provided as the first parameter in the method's call. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.split - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split - pub(crate) fn split(this: &Value, args: &[Value], context: &mut Context) -> Result { - let this = this.require_object_coercible(context)?; - let string = this.to_string(context)?; - - let separator = args.get(0).filter(|value| !value.is_null_or_undefined()); - - if let Some(result) = separator - .and_then(|separator| separator.as_object()) - .and_then(|separator| { - let key = WellKnownSymbols::split(); - - match separator.get_method(context, key) { - Ok(splitter) => splitter.map(|splitter| { - let arguments = &[ - Value::from(string.clone()), - args.get(1) - .map(|x| x.to_owned()) - .unwrap_or(Value::Undefined), - ]; - splitter.call(this, arguments, context) - }), - Err(_) => Some(Err( - context.construct_type_error("separator[Symbol.split] is not a function") - )), - } - }) - { - return result; - } - - let separator = separator - .map(|separator| separator.to_string(context)) - .transpose()?; - - let limit = args - .get(1) - .map(|arg| arg.to_integer(context).map(|limit| limit as usize)) - .transpose()? - .unwrap_or(u32::MAX as usize); - - let values: Vec = match separator { - None if limit == 0 => vec![], - None => vec![Value::from(string)], - Some(separator) if separator.is_empty() => string - .encode_utf16() - // TODO: Support keeping invalid code point in string - .map(|cp| Value::from(std::string::String::from_utf16_lossy(&[cp]))) - .take(limit) - .collect(), - Some(separator) => string - .split(separator.as_str()) - .map(&Value::from) - .take(limit) - .collect(), - }; - - let new = Array::new_array(context); - Array::construct_array(&new, &values, context) - } - - /// String.prototype.valueOf() - /// - /// The `valueOf()` method returns the primitive value of a `String` object. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf - pub(crate) fn value_of(this: &Value, args: &[Value], context: &mut Context) -> Result { - // Use the to_string method because it is specified to do the same thing in this case - Self::to_string(this, args, context) - } - - /// `String.prototype.matchAll( regexp )` - /// - /// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg]. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll - /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - /// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges - // TODO: update this method to return iterator - pub(crate) fn match_all(this: &Value, args: &[Value], context: &mut Context) -> Result { - let re: Value = match args.get(0) { - Some(arg) => { - if arg.is_null() { - RegExp::constructor( - &Value::from(Object::default()), - &[Value::from(arg.to_string(context)?), Value::from("g")], - context, - ) - } else if arg.is_undefined() { - RegExp::constructor( - &Value::from(Object::default()), - &[Value::undefined(), Value::from("g")], - context, - ) - } else { - Ok(arg.clone()) - } - } - None => RegExp::constructor( - &Value::from(Object::default()), - &[Value::from(""), Value::from("g")], - context, - ), - }?; - - RegExp::match_all(&re, this.to_string(context)?.to_string(), context) - } - - pub(crate) fn iterator(this: &Value, _: &[Value], context: &mut Context) -> Result { - StringIterator::create_string_iterator(context, this.clone()) - } -} From 8489a2c608c42257e228ad06eb6e16d660faeef0 Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Thu, 18 Feb 2021 17:04:25 +0100 Subject: [PATCH 2/8] fix: incorrect implementation of Number.prototype.toPrecision --- boa/src/builtins/number/mod.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index e1753997bcd..d336ba18d4b 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -337,7 +337,26 @@ impl Number { } } - digits.push(digit as char); + if digit as char == ':' { + // need to propagate the incrementation backward + let mut replacement = String::from("0"); + for c in digits.chars().rev() { + let d = match c { + '0'..='8' => (c as u8 + 1) as char, + _ => '0', + }; + replacement.push(d); + if d != '0' { + break; + } + } + let _trash = digits.split_off(digits.len() + 1 - replacement.len()); + for c in replacement.chars().rev() { + digits.push(c) + } + } else { + digits.push(digit as char); + } } else { digits.push_str(&"0".repeat(precision - digits.len())); } @@ -399,10 +418,8 @@ impl Number { } else { // Due to f64 limitations, this part differs a bit from the spec, // but has the same effect. It manipulates the string constructed - // by ryu-js: digits with an optional dot between two of them. - - let mut buffer = ryu_js::Buffer::new(); - suffix = buffer.format(this_num).to_string(); + // by `format`: digits with an optional dot between two of them. + suffix = format!("{:.100}", this_num); // a: getting an exponent exponent = Self::flt_str_to_exp(&suffix); @@ -449,6 +466,8 @@ impl Number { prefix.push('0'); prefix.push('.'); prefix.push_str(&"0".repeat(-e_inc as usize)); + // we have one too many precision in `suffix` + Self::round_to_precision(&mut suffix, precision - 1); } // 14 From 94e4c4f930db909360556a3cfe5f7b5b6bc2a9c3 Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Thu, 18 Feb 2021 17:05:35 +0100 Subject: [PATCH 3/8] fix: wrong float width in comment (confirmed by Value::Rational) --- boa/src/vm/instructions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/src/vm/instructions.rs b/boa/src/vm/instructions.rs index 39ea22abbd4..0b40a2334c1 100644 --- a/boa/src/vm/instructions.rs +++ b/boa/src/vm/instructions.rs @@ -12,7 +12,7 @@ pub enum Instruction { /// Loads an i32 onto the stack Int32(i32), - /// Loads an f32 onto the stack + /// Loads an f64 onto the stack Rational(f64), /// Adds the values from destination and source and stores the result in destination From 32a768259cb22934ba5f08403f48e0a854c74c1c Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Fri, 9 Apr 2021 18:10:58 +0200 Subject: [PATCH 4/8] Add more tests for Number.prototype.toPrecision --- boa/src/builtins/number/tests.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/boa/src/builtins/number/tests.rs b/boa/src/builtins/number/tests.rs index e3f8d0ae50f..ad45ffb0469 100644 --- a/boa/src/builtins/number/tests.rs +++ b/boa/src/builtins/number/tests.rs @@ -137,6 +137,8 @@ fn to_precision() { var exact_precision = (123456789).toPrecision(9); var over_precision = (123456789).toPrecision(50); var neg_precision = (-123456789).toPrecision(4); + var neg_exponent = (0.1).toPrecision(4); + var ieee754_limits = (1/3).toPrecision(60); "#; eprintln!("{}", forward(&mut context, init)); @@ -148,6 +150,8 @@ fn to_precision() { let exact_precision = forward(&mut context, "exact_precision"); let over_precision = forward(&mut context, "over_precision"); let neg_precision = forward(&mut context, "neg_precision"); + let neg_exponent = forward(&mut context, "neg_exponent"); + let ieee754_limits = forward(&mut context, "ieee754_limits"); assert_eq!(infinity, String::from("\"Infinity\"")); assert_eq!(default_precision, String::from("\"0\"")); @@ -160,6 +164,11 @@ fn to_precision() { over_precision, String::from("\"123456789.00000000000000000000000000000000000000000\"") ); + assert_eq!(neg_exponent, String::from("\"0.100\"")); + assert_eq!( + ieee754_limits, + String::from("\"0.33333333333333331482961625624739099293947219848632812500000\"") + ); let expected = "Uncaught \"RangeError\": \"precision must be an integer at least 1 and no greater than 100\""; From 58fb907c043da6c074a4cae57fb7ffde2b89158b Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Sun, 23 May 2021 10:27:17 +0200 Subject: [PATCH 5/8] Restore deleted files --- boa/src/builtins/math/mod.rs | 703 +++++++++++++++++ boa/src/builtins/string/mod.rs | 1330 ++++++++++++++++++++++++++++++++ 2 files changed, 2033 insertions(+) create mode 100644 boa/src/builtins/math/mod.rs create mode 100644 boa/src/builtins/string/mod.rs diff --git a/boa/src/builtins/math/mod.rs b/boa/src/builtins/math/mod.rs new file mode 100644 index 00000000000..633b24732f2 --- /dev/null +++ b/boa/src/builtins/math/mod.rs @@ -0,0 +1,703 @@ +//! This module implements the global `Math` object. +//! +//! `Math` is a built-in object that has properties and methods for mathematical constants and functions. It’s not a function object. +//! +//! `Math` works with the `Number` type. It doesn't work with `BigInt`. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-math-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math + +use crate::{ + builtins::BuiltIn, object::ObjectInitializer, property::Attribute, symbol::WellKnownSymbols, + BoaProfiler, Context, Result, Value, +}; + +#[cfg(test)] +mod tests; + +/// Javascript `Math` object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Math; + +impl BuiltIn for Math { + const NAME: &'static str = "Math"; + + fn attribute() -> Attribute { + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE + } + + fn init(context: &mut Context) -> (&'static str, Value, Attribute) { + let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + use std::f64; + + let to_string_tag = WellKnownSymbols::to_string_tag(); + + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; + let object = ObjectInitializer::new(context) + .property("E", f64::consts::E, attribute) + .property("LN2", f64::consts::LN_2, attribute) + .property("LN10", f64::consts::LN_10, attribute) + .property("LOG2E", f64::consts::LOG2_E, attribute) + .property("LOG10E", f64::consts::LOG10_E, attribute) + .property("SQRT1_2", 0.5_f64.sqrt(), attribute) + .property("SQRT2", f64::consts::SQRT_2, attribute) + .property("PI", f64::consts::PI, attribute) + .property( + to_string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .function(Self::abs, "abs", 1) + .function(Self::acos, "acos", 1) + .function(Self::acosh, "acosh", 1) + .function(Self::asin, "asin", 1) + .function(Self::asinh, "asinh", 1) + .function(Self::atan, "atan", 1) + .function(Self::atanh, "atanh", 1) + .function(Self::atan2, "atan2", 2) + .function(Self::cbrt, "cbrt", 1) + .function(Self::ceil, "ceil", 1) + .function(Self::clz32, "clz32", 1) + .function(Self::cos, "cos", 1) + .function(Self::cosh, "cosh", 1) + .function(Self::exp, "exp", 1) + .function(Self::expm1, "expm1", 1) + .function(Self::floor, "floor", 1) + .function(Self::fround, "fround", 1) + .function(Self::hypot, "hypot", 1) + .function(Self::imul, "imul", 1) + .function(Self::log, "log", 1) + .function(Self::log1p, "log1p", 1) + .function(Self::log10, "log10", 1) + .function(Self::log2, "log2", 1) + .function(Self::max, "max", 2) + .function(Self::min, "min", 2) + .function(Self::pow, "pow", 2) + .function(Self::random, "random", 0) + .function(Self::round, "round", 1) + .function(Self::sign, "sign", 1) + .function(Self::sin, "sin", 1) + .function(Self::sinh, "sinh", 1) + .function(Self::sqrt, "sqrt", 1) + .function(Self::tan, "tan", 1) + .function(Self::tanh, "tanh", 1) + .function(Self::trunc, "trunc", 1) + .build(); + + (Self::NAME, object.into(), Self::attribute()) + } +} + +impl Math { + /// Get the absolute value of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.abs + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs + pub(crate) fn abs(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::abs) + .into()) + } + + /// Get the arccos of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.acos + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos + pub(crate) fn acos(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::acos) + .into()) + } + + /// Get the hyperbolic arccos of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.acosh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh + pub(crate) fn acosh(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::acosh) + .into()) + } + + /// Get the arcsine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.asin + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin + pub(crate) fn asin(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::asin) + .into()) + } + + /// Get the hyperbolic arcsine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.asinh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh + pub(crate) fn asinh(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::asinh) + .into()) + } + + /// Get the arctangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.atan + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan + pub(crate) fn atan(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::atan) + .into()) + } + + /// Get the hyperbolic arctangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.atanh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh + pub(crate) fn atanh(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::atanh) + .into()) + } + + /// Get the arctangent of a numbers. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.atan2 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 + pub(crate) fn atan2(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(match ( + args.get(0).map(|x| x.to_number(context)).transpose()?, + args.get(1).map(|x| x.to_number(context)).transpose()?, + ) { + (Some(x), Some(y)) => x.atan2(y), + (_, _) => f64::NAN, + } + .into()) + } + + /// Get the cubic root of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.cbrt + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt + pub(crate) fn cbrt(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::cbrt) + .into()) + } + + /// Get lowest integer above a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.ceil + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil + pub(crate) fn ceil(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::ceil) + .into()) + } + + /// Get the number of leading zeros in the 32 bit representation of a number + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.clz32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 + pub(crate) fn clz32(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_u32(context)) + .transpose()? + .map(u32::leading_zeros) + .unwrap_or(32) + .into()) + } + + /// Get the cosine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.cos + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos + pub(crate) fn cos(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::cos) + .into()) + } + + /// Get the hyperbolic cosine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.cosh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh + pub(crate) fn cosh(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::cosh) + .into()) + } + + /// Get the power to raise the natural logarithm to get the number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.exp + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp + pub(crate) fn exp(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::exp) + .into()) + } + + /// The Math.expm1() function returns e^x - 1, where x is the argument, and e the base of + /// the natural logarithms. The result is computed in a way that is accurate even when the + /// value of x is close 0 + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.expm1 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1 + pub(crate) fn expm1(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::exp_m1) + .into()) + } + + /// Get the highest integer below a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.floor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor + pub(crate) fn floor(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::floor) + .into()) + } + + /// Get the nearest 32-bit single precision float representation of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.fround + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround + pub(crate) fn fround(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, |x| (x as f32) as f64) + .into()) + } + + /// Get an approximation of the square root of the sum of squares of all arguments. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.hypot + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot + pub(crate) fn hypot(_: &Value, args: &[Value], context: &mut Context) -> Result { + let mut result = 0f64; + for arg in args { + let x = arg.to_number(context)?; + result = result.hypot(x); + } + Ok(result.into()) + } + + /// Get the result of the C-like 32-bit multiplication of the two parameters. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.imul + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul + pub(crate) fn imul(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(match ( + args.get(0).map(|x| x.to_u32(context)).transpose()?, + args.get(1).map(|x| x.to_u32(context)).transpose()?, + ) { + (Some(x), Some(y)) => x.wrapping_mul(y) as i32, + (_, _) => 0, + } + .into()) + } + + /// Get the natural logarithm of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log + pub(crate) fn log(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, |x| if x <= 0.0 { f64::NAN } else { x.ln() }) + .into()) + } + + /// Get approximation to the natural logarithm of 1 + x. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log1p + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p + pub(crate) fn log1p(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::ln_1p) + .into()) + } + + /// Get the base 10 logarithm of the number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log10 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10 + pub(crate) fn log10(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, |x| if x <= 0.0 { f64::NAN } else { x.log10() }) + .into()) + } + + /// Get the base 2 logarithm of the number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log2 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2 + pub(crate) fn log2(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, |x| if x <= 0.0 { f64::NAN } else { x.log2() }) + .into()) + } + + /// Get the maximum of several numbers. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.max + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max + pub(crate) fn max(_: &Value, args: &[Value], context: &mut Context) -> Result { + let mut max = f64::NEG_INFINITY; + for arg in args { + let num = arg.to_number(context)?; + max = max.max(num); + } + Ok(max.into()) + } + + /// Get the minimum of several numbers. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.min + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min + pub(crate) fn min(_: &Value, args: &[Value], context: &mut Context) -> Result { + let mut min = f64::INFINITY; + for arg in args { + let num = arg.to_number(context)?; + min = min.min(num); + } + Ok(min.into()) + } + + /// Raise a number to a power. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.pow + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow + pub(crate) fn pow(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(match ( + args.get(0).map(|x| x.to_number(context)).transpose()?, + args.get(1).map(|x| x.to_number(context)).transpose()?, + ) { + (Some(x), Some(y)) => x.powf(y), + (_, _) => f64::NAN, + } + .into()) + } + + /// Generate a random floating-point number between `0` and `1`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.random + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + pub(crate) fn random(_: &Value, _: &[Value], _: &mut Context) -> Result { + Ok(rand::random::().into()) + } + + /// Round a number to the nearest integer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.round + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + pub(crate) fn round(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::round) + .into()) + } + + /// Get the sign of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sign + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign + pub(crate) fn sign(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or( + f64::NAN, + |x| { + if x == 0.0 || x == -0.0 { + x + } else { + x.signum() + } + }, + ) + .into()) + } + + /// Get the sine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sin + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin + pub(crate) fn sin(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::sin) + .into()) + } + + /// Get the hyperbolic sine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sinh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh + pub(crate) fn sinh(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::sinh) + .into()) + } + + /// Get the square root of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sqrt + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt + pub(crate) fn sqrt(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::sqrt) + .into()) + } + + /// Get the tangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.tan + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan + pub(crate) fn tan(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::tan) + .into()) + } + + /// Get the hyperbolic tangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.tanh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh + pub(crate) fn tanh(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::tanh) + .into()) + } + + /// Get the integer part of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.trunc + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc + pub(crate) fn trunc(_: &Value, args: &[Value], context: &mut Context) -> Result { + Ok(args + .get(0) + .map(|x| x.to_number(context)) + .transpose()? + .map_or(f64::NAN, f64::trunc) + .into()) + } +} diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs new file mode 100644 index 00000000000..3b6c15271af --- /dev/null +++ b/boa/src/builtins/string/mod.rs @@ -0,0 +1,1330 @@ +//! This module implements the global `String` object. +//! +//! The `String` global object is a constructor for strings or a sequence of characters. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-string-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String + +pub mod string_iterator; +#[cfg(test)] +mod tests; + +use crate::builtins::Symbol; +use crate::object::PROTOTYPE; +use crate::property::DataDescriptor; +use crate::{ + builtins::{string::string_iterator::StringIterator, Array, BuiltIn, RegExp}, + object::{ConstructorBuilder, Object, ObjectData}, + property::Attribute, + symbol::WellKnownSymbols, + value::{RcString, Value}, + BoaProfiler, Context, Result, +}; +use regress::Regex; +use std::{ + char::{decode_utf16, from_u32}, + cmp::{max, min}, + string::String as StdString, +}; + +pub(crate) fn code_point_at(string: RcString, position: i32) -> Option<(u32, u8, bool)> { + let size = string.encode_utf16().count() as i32; + if position < 0 || position >= size { + return None; + } + let mut encoded = string.encode_utf16(); + let first = encoded.nth(position as usize)?; + if !is_leading_surrogate(first) && !is_trailing_surrogate(first) { + return Some((first as u32, 1, false)); + } + if is_trailing_surrogate(first) || position + 1 == size { + return Some((first as u32, 1, true)); + } + let second = encoded.next()?; + if !is_trailing_surrogate(second) { + return Some((first as u32, 1, true)); + } + let cp = (first as u32 - 0xD800) * 0x400 + (second as u32 - 0xDC00) + 0x10000; + Some((cp, 2, false)) +} + +/// Helper function to check if a `char` is trimmable. +#[inline] +pub(crate) 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}' + ) +} + +fn is_leading_surrogate(value: u16) -> bool { + (0xD800..=0xDBFF).contains(&value) +} + +fn is_trailing_surrogate(value: u16) -> bool { + (0xDC00..=0xDFFF).contains(&value) +} + +/// JavaScript `String` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct String; + +impl BuiltIn for String { + const NAME: &'static str = "String"; + + fn attribute() -> Attribute { + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE + } + + fn init(context: &mut Context) -> (&'static str, Value, Attribute) { + let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + + let symbol_iterator = WellKnownSymbols::iterator(); + + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; + let string_object = ConstructorBuilder::with_standard_object( + context, + Self::constructor, + context.standard_objects().string_object().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .property("length", 0, attribute) + .method(Self::char_at, "charAt", 1) + .method(Self::char_code_at, "charCodeAt", 1) + .method(Self::code_point_at, "codePointAt", 1) + .method(Self::to_string, "toString", 0) + .method(Self::concat, "concat", 1) + .method(Self::repeat, "repeat", 1) + .method(Self::slice, "slice", 2) + .method(Self::starts_with, "startsWith", 1) + .method(Self::ends_with, "endsWith", 1) + .method(Self::includes, "includes", 1) + .method(Self::index_of, "indexOf", 1) + .method(Self::last_index_of, "lastIndexOf", 1) + .method(Self::r#match, "match", 1) + .method(Self::pad_end, "padEnd", 1) + .method(Self::pad_start, "padStart", 1) + .method(Self::trim, "trim", 0) + .method(Self::trim_start, "trimStart", 0) + .method(Self::trim_end, "trimEnd", 0) + .method(Self::to_lowercase, "toLowerCase", 0) + .method(Self::to_uppercase, "toUpperCase", 0) + .method(Self::substring, "substring", 2) + .method(Self::substr, "substr", 2) + .method(Self::split, "split", 2) + .method(Self::value_of, "valueOf", 0) + .method(Self::match_all, "matchAll", 1) + .method(Self::replace, "replace", 2) + .method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0) + .build(); + + (Self::NAME, string_object.into(), Self::attribute()) + } +} + +impl String { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// JavaScript strings must be between `0` and less than positive `Infinity` and cannot be a negative number. + /// The range of allowed values can be described like this: `[0, +∞)`. + /// + /// The resulting string can also not be larger than the maximum string size, + /// which can differ in JavaScript engines. In Boa it is `2^32 - 1` + pub(crate) const MAX_STRING_LENGTH: f64 = u32::MAX as f64; + + /// `String( value )` + /// + /// + pub(crate) fn constructor( + new_target: &Value, + args: &[Value], + context: &mut Context, + ) -> Result { + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + let string = match args.get(0) { + Some(value) if value.is_symbol() && new_target.is_undefined() => { + Symbol::to_string(value, &[], context)? + .as_string() + .expect("'Symbol::to_string' returns 'Value::String'") + .clone() + } + Some(ref value) => value.to_string(context)?, + None => RcString::default(), + }; + + if new_target.is_undefined() { + return Ok(string.into()); + } + let prototype = new_target + .as_object() + .and_then(|obj| { + obj.get(&PROTOTYPE.into(), obj.clone().into(), context) + .map(|o| o.as_object()) + .transpose() + }) + .transpose()? + .unwrap_or_else(|| context.standard_objects().object_object().prototype()); + let this = Value::new_object(context); + + this.as_object() + .expect("this should be an object") + .set_prototype_instance(prototype.into()); + + let length = DataDescriptor::new( + Value::from(string.encode_utf16().count()), + Attribute::NON_ENUMERABLE, + ); + this.set_property("length", length); + + this.set_data(ObjectData::String(string)); + + Ok(this) + } + + fn this_string_value(this: &Value, context: &mut Context) -> Result { + match this { + Value::String(ref string) => return Ok(string.clone()), + Value::Object(ref object) => { + let object = object.borrow(); + if let Some(string) = object.as_string() { + return Ok(string); + } + } + _ => {} + } + + Err(context.construct_type_error("'this' is not a string")) + } + + /// Get the string value to a primitive string + #[allow(clippy::wrong_self_convention)] + #[inline] + pub(crate) fn to_string(this: &Value, _: &[Value], context: &mut Context) -> Result { + // Get String from String Object and send it back as a new value + Ok(Value::from(Self::this_string_value(this, context)?)) + } + + /// `String.prototype.charAt( index )` + /// + /// The `String` object's `charAt()` method returns a new string consisting of the single UTF-16 code unit located at the specified offset into the string. + /// + /// Characters in a string are indexed from left to right. The index of the first character is `0`, + /// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`. + /// If the `index` you supply is out of this range, JavaScript returns an empty string. + /// + /// If no index is provided to `charAt()`, the default is `0`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt + pub(crate) fn char_at(this: &Value, args: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + let pos = args + .get(0) + .cloned() + .unwrap_or_else(Value::undefined) + .to_integer(context)? as i32; + + // Fast path returning empty string when pos is obviously out of range + if pos < 0 || pos >= primitive_val.len() as i32 { + return Ok("".into()); + } + + // Calling .len() on a string would give the wrong result, as they are bytes not the number of + // unicode code points + // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of + // bytes is an O(1) operation. + if let Some(utf16_val) = primitive_val.encode_utf16().nth(pos as usize) { + Ok(Value::from(from_u32(utf16_val as u32).unwrap())) + } else { + Ok("".into()) + } + } + + /// `String.prototype.codePointAt( index )` + /// + /// The `codePointAt()` method returns an integer between `0` to `1114111` (`0x10FFFF`) representing the UTF-16 code unit at the given index. + /// + /// If no UTF-16 surrogate pair begins at the index, the code point at the index is returned. + /// + /// `codePointAt()` returns `undefined` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.codepointat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt + pub(crate) fn code_point_at( + this: &Value, + args: &[Value], + context: &mut Context, + ) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + let pos = args + .get(0) + .cloned() + .unwrap_or_else(Value::undefined) + .to_integer(context)? as i32; + + // Fast path returning undefined when pos is obviously out of range + if pos < 0 || pos >= primitive_val.len() as i32 { + return Ok(Value::undefined()); + } + + if let Some((code_point, _, _)) = code_point_at(primitive_val, pos) { + Ok(Value::from(code_point)) + } else { + Ok(Value::undefined()) + } + } + + /// `String.prototype.charCodeAt( index )` + /// + /// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index. + /// + /// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding. + /// + /// `charCodeAt()` returns `NaN` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt + pub(crate) fn char_code_at( + this: &Value, + args: &[Value], + context: &mut Context, + ) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + let pos = args + .get(0) + .cloned() + .unwrap_or_else(Value::undefined) + .to_integer(context)? as i32; + + // Fast path returning NaN when pos is obviously out of range + if pos < 0 || pos >= primitive_val.len() as i32 { + return Ok(Value::nan()); + } + + // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points + // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. + // If there is no element at that index, the result is NaN + if let Some(utf16_val) = primitive_val.encode_utf16().nth(pos as usize) { + Ok(Value::from(f64::from(utf16_val))) + } else { + Ok(Value::nan()) + } + } + + /// `String.prototype.concat( str1[, ...strN] )` + /// + /// The `concat()` method concatenates the string arguments to the calling string and returns a new string. + /// + /// Changes to the original string or the returned string don't affect the other. + /// + /// If the arguments are not of the type string, they are converted to string values before concatenating. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat + pub(crate) fn concat(this: &Value, args: &[Value], context: &mut Context) -> Result { + let object = this.require_object_coercible(context)?; + let mut string = object.to_string(context)?.to_string(); + + for arg in args { + string.push_str(&arg.to_string(context)?); + } + + Ok(Value::from(string)) + } + + /// `String.prototype.repeat( count )` + /// + /// The `repeat()` method constructs and returns a new string which contains the specified number of + /// copies of the string on which it was called, concatenated together. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat + pub(crate) fn repeat(this: &Value, args: &[Value], context: &mut Context) -> Result { + let object = this.require_object_coercible(context)?; + let string = object.to_string(context)?; + + if let Some(arg) = args.get(0) { + let n = arg.to_integer(context)?; + if n < 0.0 { + return context.throw_range_error("repeat count cannot be a negative number"); + } + + if n.is_infinite() { + return context.throw_range_error("repeat count cannot be infinity"); + } + + if n * (string.len() as f64) > Self::MAX_STRING_LENGTH { + return context + .throw_range_error("repeat count must not overflow maximum string length"); + } + Ok(string.repeat(n as usize).into()) + } else { + Ok("".into()) + } + } + + /// `String.prototype.slice( beginIndex [, endIndex] )` + /// + /// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice + pub(crate) fn slice(this: &Value, args: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + + // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points + // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. + let length = primitive_val.chars().count() as i32; + + let start = args + .get(0) + .cloned() + .unwrap_or_else(Value::undefined) + .to_integer(context)? as i32; + let end = args + .get(1) + .cloned() + .unwrap_or_else(|| Value::integer(length)) + .to_integer(context)? as i32; + + let from = if start < 0 { + max(length.wrapping_add(start), 0) + } else { + min(start, length) + }; + let to = if end < 0 { + max(length.wrapping_add(end), 0) + } else { + min(end, length) + }; + + let span = max(to.wrapping_sub(from), 0); + + let new_str: StdString = primitive_val + .chars() + .skip(from as usize) + .take(span as usize) + .collect(); + Ok(Value::from(new_str)) + } + + /// `String.prototype.startWith( searchString[, position] )` + /// + /// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + pub(crate) fn starts_with( + this: &Value, + args: &[Value], + context: &mut Context, + ) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + + let arg = args.get(0).cloned().unwrap_or_else(Value::undefined); + + if Self::is_regexp_object(&arg) { + context.throw_type_error( + "First argument to String.prototype.startsWith must not be a regular expression", + )?; + } + + let search_string = arg.to_string(context)?; + + let length = primitive_val.chars().count() as i32; + let search_length = search_string.chars().count() as i32; + + // If less than 2 args specified, position is 'undefined', defaults to 0 + let position = if args.len() < 2 { + 0 + } else { + args.get(1) + .expect("failed to get arg") + .to_integer(context)? as i32 + }; + + let start = min(max(position, 0), length); + let end = start.wrapping_add(search_length); + + if end > length { + Ok(Value::from(false)) + } else { + // Only use the part of the string from "start" + let this_string: StdString = primitive_val.chars().skip(start as usize).collect(); + Ok(Value::from(this_string.starts_with(search_string.as_str()))) + } + } + + /// `String.prototype.endsWith( searchString[, length] )` + /// + /// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith + pub(crate) fn ends_with(this: &Value, args: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + + let arg = args.get(0).cloned().unwrap_or_else(Value::undefined); + + if Self::is_regexp_object(&arg) { + context.throw_type_error( + "First argument to String.prototype.endsWith must not be a regular expression", + )?; + } + + let search_string = arg.to_string(context)?; + + let length = primitive_val.chars().count() as i32; + let search_length = search_string.chars().count() as i32; + + // If less than 2 args specified, end_position is 'undefined', defaults to + // length of this + let end_position = if args.len() < 2 { + length + } else { + args.get(1) + .expect("Could not get argumetn") + .to_integer(context)? as i32 + }; + + let end = min(max(end_position, 0), length); + let start = end.wrapping_sub(search_length); + + if start < 0 { + Ok(Value::from(false)) + } else { + // Only use the part of the string up to "end" + let this_string: StdString = primitive_val.chars().take(end as usize).collect(); + Ok(Value::from(this_string.ends_with(search_string.as_str()))) + } + } + + /// `String.prototype.includes( searchString[, position] )` + /// + /// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes + pub(crate) fn includes(this: &Value, args: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + + let arg = args.get(0).cloned().unwrap_or_else(Value::undefined); + + if Self::is_regexp_object(&arg) { + context.throw_type_error( + "First argument to String.prototype.includes must not be a regular expression", + )?; + } + + let search_string = arg.to_string(context)?; + + let length = primitive_val.chars().count() as i32; + + // If less than 2 args specified, position is 'undefined', defaults to 0 + let position = if args.len() < 2 { + 0 + } else { + args.get(1) + .expect("Could not get argument") + .to_integer(context)? as i32 + }; + + let start = min(max(position, 0), length); + + // Take the string from "this" and use only the part of it after "start" + let this_string: StdString = primitive_val.chars().skip(start as usize).collect(); + + Ok(Value::from(this_string.contains(search_string.as_str()))) + } + + /// Return either the string itself or the string of the regex equivalent + fn get_regex_string(value: &Value) -> StdString { + match value { + Value::String(ref body) => body.to_string(), + Value::Object(ref obj) => { + let obj = obj.borrow(); + + if let Some(regexp) = obj.as_regexp() { + // first argument is another `RegExp` object, so copy its pattern and flags + return regexp.original_source.clone().into(); + } + "undefined".to_string() + } + _ => "undefined".to_string(), + } + } + + fn is_regexp_object(value: &Value) -> bool { + match value { + Value::Object(ref obj) => obj.borrow().is_regexp(), + _ => false, + } + } + + /// `String.prototype.replace( regexp|substr, newSubstr|function )` + /// + /// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`. + /// + /// The `pattern` can be a string or a `RegExp`, and the `replacement` can be a string or a function to be called for each match. + /// If `pattern` is a string, only the first occurrence will be replaced. + /// + /// The original string is left unchanged. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + pub(crate) fn replace(this: &Value, args: &[Value], context: &mut Context) -> Result { + // TODO: Support Symbol replacer + let primitive_val = this.to_string(context)?; + if args.is_empty() { + return Ok(Value::from(primitive_val)); + } + + let regex_body = Self::get_regex_string(args.get(0).expect("Value needed")); + let re = Regex::new(®ex_body).expect("unable to convert regex to regex object"); + let mat = match re.find(&primitive_val) { + Some(mat) => mat, + None => return Ok(Value::from(primitive_val)), + }; + let caps = re + .find(&primitive_val) + .expect("unable to get capture groups from text") + .captures; + + let replace_value = if args.len() > 1 { + // replace_object could be a string or function or not exist at all + let replace_object: &Value = args.get(1).expect("second argument expected"); + match replace_object { + Value::String(val) => { + // https://tc39.es/ecma262/#table-45 + let mut result = StdString::new(); + let mut chars = val.chars().peekable(); + + let m = caps.len(); + + while let Some(first) = chars.next() { + if first == '$' { + let second = chars.next(); + let second_is_digit = second.map_or(false, |ch| ch.is_digit(10)); + // we use peek so that it is still in the iterator if not used + let third = if second_is_digit { chars.peek() } else { None }; + let third_is_digit = third.map_or(false, |ch| ch.is_digit(10)); + + match (second, third) { + (Some('$'), _) => { + // $$ + result.push('$'); + } + (Some('&'), _) => { + // $& + result.push_str(&primitive_val[mat.range()]); + } + (Some('`'), _) => { + // $` + let start_of_match = mat.start(); + result.push_str(&primitive_val[..start_of_match]); + } + (Some('\''), _) => { + // $' + let end_of_match = mat.end(); + result.push_str(&primitive_val[end_of_match..]); + } + (Some(second), Some(third)) + if second_is_digit && third_is_digit => + { + // $nn + let tens = second.to_digit(10).unwrap() as usize; + let units = third.to_digit(10).unwrap() as usize; + let nn = 10 * tens + units; + if nn == 0 || nn > m { + result.push(first); + result.push(second); + if let Some(ch) = chars.next() { + result.push(ch); + } + } else { + let group = match mat.group(nn) { + Some(range) => &primitive_val[range.clone()], + _ => "", + }; + result.push_str(group); + chars.next(); // consume third + } + } + (Some(second), _) if second_is_digit => { + // $n + let n = second.to_digit(10).unwrap() as usize; + if n == 0 || n > m { + result.push(first); + result.push(second); + } else { + let group = match mat.group(n) { + Some(range) => &primitive_val[range.clone()], + _ => "", + }; + result.push_str(group); + } + } + (Some('<'), _) => { + // $< + // TODO: named capture groups + result.push_str("$<"); + } + _ => { + // $?, ? is none of the above + // we can consume second because it isn't $ + result.push(first); + if let Some(second) = second { + result.push(second); + } + } + } + } else { + result.push(first); + } + } + + result + } + Value::Object(_) => { + // This will return the matched substring first, then captured parenthesized groups later + let mut results: Vec = mat + .groups() + .map(|group| match group { + Some(range) => Value::from(&primitive_val[range]), + None => Value::undefined(), + }) + .collect(); + + // Returns the starting byte offset of the match + let start = mat.start(); + results.push(Value::from(start)); + // Push the whole string being examined + results.push(Value::from(primitive_val.to_string())); + + let result = context.call(&replace_object, this, &results)?; + + result.to_string(context)?.to_string() + } + _ => "undefined".to_string(), + } + } else { + "undefined".to_string() + }; + + Ok(Value::from(primitive_val.replacen( + &primitive_val[mat.range()], + &replace_value, + 1, + ))) + } + + /// `String.prototype.indexOf( searchValue[, fromIndex] )` + /// + /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence + /// of the specified value, starting the search at `fromIndex`. + /// + /// Returns `-1` if the value is not found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf + pub(crate) fn index_of(this: &Value, args: &[Value], context: &mut Context) -> Result { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + + let search_string = args + .get(0) + .cloned() + .unwrap_or_else(Value::undefined) + .to_string(context)?; + + let length = string.chars().count(); + let start = args + .get(1) + .map(|position| position.to_integer(context)) + .transpose()? + .map_or(0, |position| position.max(0.0).min(length as f64) as usize); + + if search_string.is_empty() { + return Ok(start.min(length).into()); + } + + if start < length { + if let Some(position) = string.find(search_string.as_str()) { + return Ok(string[..position].chars().count().into()); + } + } + + Ok(Value::from(-1)) + } + + /// `String.prototype.lastIndexOf( searchValue[, fromIndex] )` + /// + /// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence + /// of the specified value, searching backwards from `fromIndex`. + /// + /// Returns `-1` if the value is not found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf + pub(crate) fn last_index_of( + this: &Value, + args: &[Value], + context: &mut Context, + ) -> Result { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + + let search_string = args + .get(0) + .cloned() + .unwrap_or_else(Value::undefined) + .to_string(context)?; + + let length = string.chars().count(); + let start = args + .get(1) + .map(|position| position.to_integer(context)) + .transpose()? + .map_or(0, |position| position.max(0.0).min(length as f64) as usize); + + if search_string.is_empty() { + return Ok(start.min(length).into()); + } + + if start < length { + if let Some(position) = string.rfind(search_string.as_str()) { + return Ok(string[..position].chars().count().into()); + } + } + + Ok(Value::from(-1)) + } + + /// `String.prototype.match( regexp )` + /// + /// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match + /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + pub(crate) fn r#match(this: &Value, args: &[Value], context: &mut Context) -> Result { + let re = RegExp::constructor( + &Value::from(Object::default()), + &[args.get(0).cloned().unwrap_or_default()], + context, + )?; + RegExp::r#match(&re, this.to_string(context)?, context) + } + + /// Abstract method `StringPad`. + /// + /// Performs the actual string padding for padStart/End. + /// + fn string_pad( + primitive: RcString, + max_length: i32, + fill_string: Option, + at_start: bool, + ) -> Value { + let primitive_length = primitive.len() as i32; + + if max_length <= primitive_length { + return Value::from(primitive); + } + + let filter = fill_string.as_deref().unwrap_or(" "); + + let fill_len = max_length.wrapping_sub(primitive_length); + let mut fill_str = StdString::new(); + + while fill_str.len() < fill_len as usize { + fill_str.push_str(filter); + } + // Cut to size max_length + let concat_fill_str: StdString = fill_str.chars().take(fill_len as usize).collect(); + + if at_start { + Value::from(format!("{}{}", concat_fill_str, &primitive)) + } else { + Value::from(format!("{}{}", primitive, &concat_fill_str)) + } + } + + /// `String.prototype.padEnd( targetLength[, padString] )` + /// + /// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length. + /// + /// The padding is applied from the end of the current string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd + pub(crate) fn pad_end(this: &Value, args: &[Value], context: &mut Context) -> Result { + let primitive = this.to_string(context)?; + if args.is_empty() { + return Err(Value::from("padEnd requires maxLength argument")); + } + let max_length = args + .get(0) + .expect("failed to get argument for String method") + .to_integer(context)? as i32; + + let fill_string = args.get(1).map(|arg| arg.to_string(context)).transpose()?; + + Ok(Self::string_pad(primitive, max_length, fill_string, false)) + } + + /// `String.prototype.padStart( targetLength [, padString] )` + /// + /// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length. + /// + /// The padding is applied from the start of the current string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart + pub(crate) fn pad_start(this: &Value, args: &[Value], context: &mut Context) -> Result { + let primitive = this.to_string(context)?; + if args.is_empty() { + return Err(Value::from("padStart requires maxLength argument")); + } + let max_length = args + .get(0) + .expect("failed to get argument for String method") + .to_integer(context)? as i32; + + let fill_string = args.get(1).map(|arg| arg.to_string(context)).transpose()?; + + Ok(Self::string_pad(primitive, max_length, fill_string, true)) + } + + /// String.prototype.trim() + /// + /// The `trim()` method removes whitespace from both ends of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim + pub(crate) fn trim(this: &Value, _: &[Value], context: &mut Context) -> Result { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + Ok(Value::from(string.trim_matches(is_trimmable_whitespace))) + } + + /// `String.prototype.trimStart()` + /// + /// The `trimStart()` method removes whitespace from the beginning of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart + pub(crate) fn trim_start(this: &Value, _: &[Value], context: &mut Context) -> Result { + let string = this.to_string(context)?; + Ok(Value::from( + string.trim_start_matches(is_trimmable_whitespace), + )) + } + + /// String.prototype.trimEnd() + /// + /// The `trimEnd()` method removes whitespace from the end of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd + pub(crate) fn trim_end(this: &Value, _: &[Value], context: &mut Context) -> Result { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + Ok(Value::from( + string.trim_end_matches(is_trimmable_whitespace), + )) + } + + /// `String.prototype.toLowerCase()` + /// + /// The `toLowerCase()` method returns the calling string value converted to lower case. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tolowercase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_lowercase(this: &Value, _: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let this_str = this.to_string(context)?; + // The Rust String is mapped to uppercase using the builtin .to_lowercase(). + // There might be corner cases where it does not behave exactly like Javascript expects + Ok(Value::from(this_str.to_lowercase())) + } + + /// `String.prototype.toUpperCase()` + /// + /// The `toUpperCase()` method returns the calling string value converted to uppercase. + /// + /// The value will be **converted** to a string if it isn't one + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_uppercase(this: &Value, _: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let this_str = this.to_string(context)?; + // The Rust String is mapped to uppercase using the builtin .to_uppercase(). + // There might be corner cases where it does not behave exactly like Javascript expects + Ok(Value::from(this_str.to_uppercase())) + } + + /// `String.prototype.substring( indexStart[, indexEnd] )` + /// + /// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring + pub(crate) fn substring(this: &Value, args: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + // If no args are specified, start is 'undefined', defaults to 0 + let start = if args.is_empty() { + 0 + } else { + args.get(0) + .expect("failed to get argument for String method") + .to_integer(context)? as i32 + }; + let length = primitive_val.encode_utf16().count() as i32; + // If less than 2 args specified, end is the length of the this object converted to a String + let end = if args.len() < 2 { + length + } else { + args.get(1) + .expect("Could not get argument") + .to_integer(context)? as i32 + }; + // Both start and end args replaced by 0 if they were negative + // or by the length of the String if they were greater + let final_start = min(max(start, 0), length); + let final_end = min(max(end, 0), length); + // Start and end are swapped if start is greater than end + let from = min(final_start, final_end) as usize; + let to = max(final_start, final_end) as usize; + // Extract the part of the string contained between the start index and the end index + // where start is guaranteed to be smaller or equals to end + let extracted_string: std::result::Result = decode_utf16( + primitive_val + .encode_utf16() + .skip(from) + .take(to.wrapping_sub(from)), + ) + .collect(); + Ok(Value::from(extracted_string.expect("Invalid string"))) + } + + /// `String.prototype.substr( start[, length] )` + /// + /// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr + /// + pub(crate) fn substr(this: &Value, args: &[Value], context: &mut Context) -> Result { + // First we get it the actual string a private field stored on the object only the context has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = this.to_string(context)?; + // If no args are specified, start is 'undefined', defaults to 0 + let mut start = if args.is_empty() { + 0 + } else { + args.get(0) + .expect("failed to get argument for String method") + .to_integer(context)? as i32 + }; + let length = primitive_val.chars().count() as i32; + // If less than 2 args specified, end is +infinity, the maximum number value. + // Using i32::max_value() should be safe because the final length used is at most + // the number of code units from start to the end of the string, + // which should always be smaller or equals to both +infinity and i32::max_value + let end = if args.len() < 2 { + i32::MAX + } else { + args.get(1) + .expect("Could not get argument") + .to_integer(context)? as i32 + }; + // If start is negative it become the number of code units from the end of the string + if start < 0 { + start = max(length.wrapping_add(start), 0); + } + // length replaced by 0 if it was negative + // or by the number of code units from start to the end of the string if it was greater + let result_length = min(max(end, 0), length.wrapping_sub(start)); + // If length is negative we return an empty string + // otherwise we extract the part of the string from start and is length code units long + if result_length <= 0 { + Ok(Value::from("")) + } else { + let extracted_string: StdString = primitive_val + .chars() + .skip(start as usize) + .take(result_length as usize) + .collect(); + + Ok(Value::from(extracted_string)) + } + } + + /// String.prototype.split() + /// + /// The `split()` method divides a String into an ordered list of substrings, puts these substrings into an array, and returns the array. + /// + /// The division is done by searching for a pattern; where the pattern is provided as the first parameter in the method's call. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.split + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split + pub(crate) fn split(this: &Value, args: &[Value], context: &mut Context) -> Result { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + + let separator = args.get(0).filter(|value| !value.is_null_or_undefined()); + + if let Some(result) = separator + .and_then(|separator| separator.as_object()) + .and_then(|separator| { + let key = WellKnownSymbols::split(); + + match separator.get_method(context, key) { + Ok(splitter) => splitter.map(|splitter| { + let arguments = &[ + Value::from(string.clone()), + args.get(1) + .map(|x| x.to_owned()) + .unwrap_or(Value::Undefined), + ]; + splitter.call(this, arguments, context) + }), + Err(_) => Some(Err( + context.construct_type_error("separator[Symbol.split] is not a function") + )), + } + }) + { + return result; + } + + let separator = separator + .map(|separator| separator.to_string(context)) + .transpose()?; + + let limit = args + .get(1) + .map(|arg| arg.to_integer(context).map(|limit| limit as usize)) + .transpose()? + .unwrap_or(u32::MAX as usize); + + let values: Vec = match separator { + None if limit == 0 => vec![], + None => vec![Value::from(string)], + Some(separator) if separator.is_empty() => string + .encode_utf16() + // TODO: Support keeping invalid code point in string + .map(|cp| Value::from(std::string::String::from_utf16_lossy(&[cp]))) + .take(limit) + .collect(), + Some(separator) => string + .split(separator.as_str()) + .map(&Value::from) + .take(limit) + .collect(), + }; + + let new = Array::new_array(context); + Array::construct_array(&new, &values, context) + } + + /// String.prototype.valueOf() + /// + /// The `valueOf()` method returns the primitive value of a `String` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf + pub(crate) fn value_of(this: &Value, args: &[Value], context: &mut Context) -> Result { + // Use the to_string method because it is specified to do the same thing in this case + Self::to_string(this, args, context) + } + + /// `String.prototype.matchAll( regexp )` + /// + /// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll + /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + /// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges + // TODO: update this method to return iterator + pub(crate) fn match_all(this: &Value, args: &[Value], context: &mut Context) -> Result { + let re: Value = match args.get(0) { + Some(arg) => { + if arg.is_null() { + RegExp::constructor( + &Value::from(Object::default()), + &[Value::from(arg.to_string(context)?), Value::from("g")], + context, + ) + } else if arg.is_undefined() { + RegExp::constructor( + &Value::from(Object::default()), + &[Value::undefined(), Value::from("g")], + context, + ) + } else { + Ok(arg.clone()) + } + } + None => RegExp::constructor( + &Value::from(Object::default()), + &[Value::from(""), Value::from("g")], + context, + ), + }?; + + RegExp::match_all(&re, this.to_string(context)?.to_string(), context) + } + + pub(crate) fn iterator(this: &Value, _: &[Value], context: &mut Context) -> Result { + StringIterator::create_string_iterator(context, this.clone()) + } +} From 16129523320e672f1b4ec0eaaf7e2bb67aed55c9 Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Sun, 23 May 2021 17:19:09 +0200 Subject: [PATCH 6/8] fix: removed far-rounding for test262 compiance; fixed wrong exponent computations --- boa/src/builtins/number/mod.rs | 48 +++++++++++++++++++------------- boa/src/builtins/number/tests.rs | 4 +-- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index d336ba18d4b..481bf2a9881 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -318,47 +318,57 @@ impl Number { /// represented by these digits is rounded using string /// manipulation. /// - Else, zeroes are appended to the string. + /// - Additionnally, sometimes the exponent was wrongly computed and + /// while up-rounding we find that we need an extra digit. When this + /// happens, we return true so that the calling context can adjust + /// the exponent. The string is kept at an exact length of `precision`. /// /// When this procedure returns, `digits` is exactly `precision` long. /// - fn round_to_precision(digits: &mut String, precision: usize) { + fn round_to_precision(digits: &mut String, precision: usize) -> bool { if digits.len() > precision { let to_round = digits.split_off(precision); let mut digit = digits.pop().unwrap() as u8; - - for c in to_round.chars() { - match c { - c if c < '4' => break, - c if c > '4' => { - digit += 1; - break; - } - _ => {} + if let Some(first) = to_round.chars().next() { + if first > '4' { + digit += 1; } } if digit as char == ':' { - // need to propagate the incrementation backward + // ':' is '9' + 1 + // need to propagate the increment backward let mut replacement = String::from("0"); + let mut propagated = false; for c in digits.chars().rev() { - let d = match c { - '0'..='8' => (c as u8 + 1) as char, - _ => '0', + let d = match (c, propagated) { + ('0'..='8', false) => (c as u8 + 1) as char, + (_, false) => '0', + (_, true) => c, }; replacement.push(d); if d != '0' { - break; + propagated = true; } } - let _trash = digits.split_off(digits.len() + 1 - replacement.len()); + digits.clear(); + let replacement = if !propagated { + digits.push('1'); + &replacement.as_str()[1..] + } else { + replacement.as_str() + }; for c in replacement.chars().rev() { digits.push(c) } + !propagated } else { digits.push(digit as char); + false } } else { digits.push_str(&"0".repeat(precision - digits.len())); + false } } @@ -430,7 +440,9 @@ impl Number { suffix.remove(n); } // impl: having exactly `precision` digits in `suffix` - Self::round_to_precision(&mut suffix, precision); + if Self::round_to_precision(&mut suffix, precision) { + exponent += 1; + } // c: switching to scientific notation let great_exp = exponent >= precision_i32; @@ -466,8 +478,6 @@ impl Number { prefix.push('0'); prefix.push('.'); prefix.push_str(&"0".repeat(-e_inc as usize)); - // we have one too many precision in `suffix` - Self::round_to_precision(&mut suffix, precision - 1); } // 14 diff --git a/boa/src/builtins/number/tests.rs b/boa/src/builtins/number/tests.rs index ad45ffb0469..634c527963c 100644 --- a/boa/src/builtins/number/tests.rs +++ b/boa/src/builtins/number/tests.rs @@ -164,10 +164,10 @@ fn to_precision() { over_precision, String::from("\"123456789.00000000000000000000000000000000000000000\"") ); - assert_eq!(neg_exponent, String::from("\"0.100\"")); + assert_eq!(neg_exponent, String::from("\"0.1000\"")); assert_eq!( ieee754_limits, - String::from("\"0.33333333333333331482961625624739099293947219848632812500000\"") + String::from("\"0.333333333333333314829616256247390992939472198486328125000000\"") ); let expected = "Uncaught \"RangeError\": \"precision must be an integer at least 1 and no greater than 100\""; From 5e6f56ecf46820b47b3e16b56a5d3314d69c9a53 Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Sun, 23 May 2021 17:19:36 +0200 Subject: [PATCH 7/8] fix: operations order when detecting NaN --- boa/src/builtins/number/mod.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index 481bf2a9881..986f8f4785f 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -392,8 +392,8 @@ impl Number { // 1 & 6 let mut this_num = Self::this_number_value(this, context)?; - // 2 & 4 - if precision_var == Value::undefined() || !this_num.is_finite() { + // 2 + if precision_var == Value::undefined() { return Self::to_string(this, &[], context); } @@ -409,10 +409,15 @@ impl Number { }; let precision_i32 = precision as i32; + // 4 + if !this_num.is_finite() { + return Self::to_string(this, &[], context); + } + // 7 let mut prefix = String::new(); // spec: 's' let mut suffix: String; // spec: 'm' - let exponent: i32; // spec: 'e' + let mut exponent: i32; // spec: 'e' // 8 if this_num < 0.0 { From 2c821a03f10ebbfa2885e4f0c658a41ec938bb43 Mon Sep 17 00:00:00 2001 From: NathanRoyer Date: Sun, 23 May 2021 17:43:25 +0200 Subject: [PATCH 8/8] fir operations order when restricting precision range --- boa/src/builtins/number/mod.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index 986f8f4785f..d973baba4ba 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -388,17 +388,24 @@ impl Number { args: &[Value], context: &mut Context, ) -> Result { - let precision_var = args.get(0).cloned().unwrap_or_default(); + let precision = args.get(0).cloned().unwrap_or_default(); // 1 & 6 let mut this_num = Self::this_number_value(this, context)?; // 2 - if precision_var == Value::undefined() { + if precision == Value::undefined() { return Self::to_string(this, &[], context); } // 3 - let precision = match precision_var.to_integer_or_infinity(context)? { + let precision = precision.to_integer_or_infinity(context)?; + + // 4 + if !this_num.is_finite() { + return Self::to_string(this, &[], context); + } + + let precision = match precision { IntegerOrInfinity::Integer(x) if (1..=100).contains(&x) => x as usize, _ => { // 5 @@ -409,11 +416,6 @@ impl Number { }; let precision_i32 = precision as i32; - // 4 - if !this_num.is_finite() { - return Self::to_string(this, &[], context); - } - // 7 let mut prefix = String::new(); // spec: 's' let mut suffix: String; // spec: 'm'