diff --git a/Cargo.lock b/Cargo.lock index 7cfcd3a664..b205cea5d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,6 +7,7 @@ dependencies = [ "criterion", "gc", "jemallocator", + "num-traits", "rand", "regex", "rustc-hash", diff --git a/boa/Cargo.toml b/boa/Cargo.toml index e8428876ed..dfd749beb7 100644 --- a/boa/Cargo.toml +++ b/boa/Cargo.toml @@ -14,6 +14,7 @@ edition = "2018" gc = { version = "0.3.4", features = ["derive"] } serde_json = "1.0.52" rand = "0.7.3" +num-traits = "0.2.11" regex = "1.3.7" rustc-hash = "1.1.0" diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index dac495980e..d9e864ece5 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -23,6 +23,7 @@ use crate::{ }, exec::Interpreter, }; +use num_traits::float::FloatCore; use std::{borrow::Borrow, f64, ops::Deref}; /// Helper function that converts a Value to a Number. @@ -159,6 +160,129 @@ pub fn to_precision(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> unimplemented!("TODO: Implement toPrecision"); } +const BUF_SIZE: usize = 2200; + +// https://golang.org/src/math/nextafter.go +#[inline] +fn next_after(x: f64, y: f64) -> f64 { + if x.is_nan() || y.is_nan() { + f64::NAN + } else if (x - y) == 0. { + x + } else if x == 0.0 { + f64::from_bits(1).copysign(y) + } else if y > x || x > 0.0 { + f64::from_bits(x.to_bits() + 1) + } else { + f64::from_bits(x.to_bits() - 1) + } +} + +// https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/numbers/conversions.cc#1230 +pub fn num_to_string(mut value: f64, radix: u8) -> String { + assert!(radix >= 2); + assert!(radix <= 36); + assert!(value.is_finite()); + // assert_ne!(0.0, value); + + // Character array used for conversion. + // Temporary buffer for the result. We start with the decimal point in the + // middle and write to the left for the integer part and to the right for the + // fractional part. 1024 characters for the exponent and 52 for the mantissa + // either way, with additional space for sign, decimal point and string + // termination should be sufficient. + let mut buffer: [u8; BUF_SIZE] = [0; BUF_SIZE]; + let (int_buf, frac_buf) = buffer.split_at_mut(BUF_SIZE / 2); + let mut fraction_cursor = 0; + let negative = value.is_sign_negative(); + if negative { + value = -value + } + // Split the value into an integer part and a fractional part. + // let mut integer = value.trunc(); + // let mut fraction = value.fract(); + let mut integer = value.floor(); + let mut fraction = value - integer; + + // We only compute fractional digits up to the input double's precision. + let mut delta = 0.5 * (next_after(value, f64::MAX) - value); + delta = next_after(0.0, f64::MAX).max(delta); + assert!(delta > 0.0); + if fraction >= delta { + // Insert decimal point. + frac_buf[fraction_cursor] = b'.'; + fraction_cursor += 1; + loop { + // Shift up by one digit. + fraction *= radix as f64; + delta *= radix as f64; + // Write digit. + let digit = fraction as u32; + frac_buf[fraction_cursor] = std::char::from_digit(digit, radix as u32).unwrap() as u8; + fraction_cursor += 1; + // Calculate remainder. + fraction -= digit as f64; + // Round to even. + if fraction + delta > 1.0 + && (fraction > 0.5 || (fraction - 0.5) < f64::EPSILON && digit & 1 != 0) + { + loop { + // We need to back trace already written digits in case of carry-over. + fraction_cursor -= 1; + if fraction_cursor == 0 { + // CHECK_EQ('.', buffer[fraction_cursor]); + // Carry over to the integer part. + integer += 1.; + break; + } else { + let c: u8 = frac_buf[fraction_cursor]; + // Reconstruct digit. + let digit_0 = (c as char).to_digit(10).unwrap(); + if digit_0 + 1 >= radix as u32 { + continue; + } + frac_buf[fraction_cursor] = + std::char::from_digit(digit_0 + 1, radix as u32).unwrap() as u8; + fraction_cursor += 1; + break; + } + } + break; + } + if fraction < delta { + break; + } + } + } + + // Compute integer digits. Fill unrepresented digits with zero. + let mut int_iter = int_buf.iter_mut().enumerate().rev(); //.rev(); + while FloatCore::integer_decode(integer / f64::from(radix)).1 > 0 { + integer /= radix as f64; + *int_iter.next().unwrap().1 = b'0'; + } + + loop { + let remainder = integer % (radix as f64); + *int_iter.next().unwrap().1 = + std::char::from_digit(remainder as u32, radix as u32).unwrap() as u8; + integer = (integer - remainder) / radix as f64; + if integer <= 0f64 { + break; + } + } + // Add sign and terminate string. + if negative { + *int_iter.next().unwrap().1 = b'-'; + } + assert!(fraction_cursor < BUF_SIZE); + + let integer_cursor = int_iter.next().unwrap().0 + 1; + let fraction_cursor = fraction_cursor + BUF_SIZE / 2; + // dbg!("Number: {}, Radix: {}, Cursors: {}, {}", value, radix, integer_cursor, fraction_cursor); + String::from_utf8_lossy(&buffer[integer_cursor..fraction_cursor]).into() +} + /// `Number.prototype.toString( [radix] )` /// /// The `toString()` method returns a string representing the specified Number object. @@ -169,8 +293,45 @@ pub fn to_precision(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> /// /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tostring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString -pub fn to_string(this: &mut Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - Ok(Value::from(format!("{}", to_number(this).to_number()))) +pub fn to_string(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + // 1. Let x be ? thisNumberValue(this value). + let x = to_number(this).to_number(); + // 2. If radix is undefined, let radixNumber be 10. + // 3. Else, let radixNumber be ? ToInteger(radix). + let radix_number = args.get(0).map_or(10, |arg| arg.to_integer()) as u8; + + if x == -0. { + return Ok(Value::from("0")); + } else if x.is_nan() { + return Ok(Value::from("NaN")); + } else if x.is_infinite() && x.is_sign_positive() { + return Ok(Value::from("Infinity")); + } else if x.is_infinite() && x.is_sign_negative() { + return Ok(Value::from("-Infinity")); + } + + // 4. If radixNumber < 2 or radixNumber > 36, throw a RangeError exception. + if radix_number < 2 || radix_number > 36 { + panic!("Radix must be between 2 and 36"); + } + + // 5. If radixNumber = 10, return ! ToString(x). + // This part should use exponential notations for long integer numbers commented tests + if radix_number == 10 { + // return Ok(to_value(format!("{}", to_number(this).to_num()))); + return Ok(Value::from(format!("{}", x))); + } + + // This is a Optimization from the v8 source code to print values that can fit in a single character + // Since the actual num_to_string allocates a 2200 bytes buffer for actual conversion + // I am not sure if this part is effective as the v8 equivalent https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/builtins/number.tq#53 + // // Fast case where the result is a one character string. + // if x.is_sign_positive() && x.fract() == 0.0 && x < radix_number as f64 { + // return Ok(to_value(format!("{}", std::char::from_digit(x as u32, radix_number as u32).unwrap()))) + // } + + // 6. Return the String representation of this Number value using the radix specified by radixNumber. + Ok(Value::from(num_to_string(x, radix_number))) } /// `Number.prototype.toString()` diff --git a/boa/src/builtins/number/tests.rs b/boa/src/builtins/number/tests.rs index fc9aea1cbc..e4cb7a4d2a 100644 --- a/boa/src/builtins/number/tests.rs +++ b/boa/src/builtins/number/tests.rs @@ -162,26 +162,210 @@ fn to_precision() { fn to_string() { let realm = Realm::create(); let mut engine = Executor::new(realm); - let init = r#" - var default_string = Number().toString(); - var int_string = Number(123).toString(); - var float_string = Number(1.234).toString(); - var exp_string = Number("1.2e+4").toString(); - var neg_string = Number(-1.2).toString(); - "#; - eprintln!("{}", forward(&mut engine, init)); - let default_string = forward(&mut engine, "default_string"); - let int_string = forward(&mut engine, "int_string"); - let float_string = forward(&mut engine, "float_string"); - let exp_string = forward(&mut engine, "exp_string"); - let neg_string = forward(&mut engine, "neg_string"); - - assert_eq!(default_string, String::from("0")); - assert_eq!(int_string, String::from("123")); - assert_eq!(float_string, String::from("1.234")); - assert_eq!(exp_string, String::from("12000")); - assert_eq!(neg_string, String::from("-1.2")); + assert_eq!("NaN", &forward(&mut engine, "Number(NaN).toString()")); + assert_eq!("Infinity", &forward(&mut engine, "Number(1/0).toString()")); + assert_eq!( + "-Infinity", + &forward(&mut engine, "Number(-1/0).toString()") + ); + assert_eq!("0", &forward(&mut engine, "Number(0).toString()")); + assert_eq!("9", &forward(&mut engine, "Number(9).toString()")); + assert_eq!("90", &forward(&mut engine, "Number(90).toString()")); + assert_eq!("90.12", &forward(&mut engine, "Number(90.12).toString()")); + assert_eq!("0.1", &forward(&mut engine, "Number(0.1).toString()")); + assert_eq!("0.01", &forward(&mut engine, "Number(0.01).toString()")); + assert_eq!("0.0123", &forward(&mut engine, "Number(0.0123).toString()")); + assert_eq!( + "0.00001", + &forward(&mut engine, "Number(0.00001).toString()") + ); + assert_eq!( + "0.000001", + &forward(&mut engine, "Number(0.000001).toString()") + ); + assert_eq!("NaN", &forward(&mut engine, "Number(NaN).toString(16)")); + assert_eq!( + "Infinity", + &forward(&mut engine, "Number(1/0).toString(16)") + ); + assert_eq!( + "-Infinity", + &forward(&mut engine, "Number(-1/0).toString(16)") + ); + assert_eq!("0", &forward(&mut engine, "Number(0).toString(16)")); + assert_eq!("9", &forward(&mut engine, "Number(9).toString(16)")); + assert_eq!("5a", &forward(&mut engine, "Number(90).toString(16)")); + assert_eq!( + "5a.1eb851eb852", + &forward(&mut engine, "Number(90.12).toString(16)") + ); + assert_eq!( + "0.1999999999999a", + &forward(&mut engine, "Number(0.1).toString(16)") + ); + assert_eq!( + "0.028f5c28f5c28f6", + &forward(&mut engine, "Number(0.01).toString(16)") + ); + assert_eq!( + "0.032617c1bda511a", + &forward(&mut engine, "Number(0.0123).toString(16)") + ); + assert_eq!( + "605f9f6dd18bc8000", + &forward(&mut engine, "Number(111111111111111111111).toString(16)") + ); + assert_eq!( + "3c3bc3a4a2f75c0000", + &forward(&mut engine, "Number(1111111111111111111111).toString(16)") + ); + assert_eq!( + "25a55a46e5da9a00000", + &forward(&mut engine, "Number(11111111111111111111111).toString(16)") + ); + assert_eq!( + "0.0000a7c5ac471b4788", + &forward(&mut engine, "Number(0.00001).toString(16)") + ); + assert_eq!( + "0.000010c6f7a0b5ed8d", + &forward(&mut engine, "Number(0.000001).toString(16)") + ); + assert_eq!( + "0.000001ad7f29abcaf48", + &forward(&mut engine, "Number(0.0000001).toString(16)") + ); + assert_eq!( + "0.000002036565348d256", + &forward(&mut engine, "Number(0.00000012).toString(16)") + ); + assert_eq!( + "0.0000021047ee22aa466", + &forward(&mut engine, "Number(0.000000123).toString(16)") + ); + assert_eq!( + "0.0000002af31dc4611874", + &forward(&mut engine, "Number(0.00000001).toString(16)") + ); + assert_eq!( + "0.000000338a23b87483be", + &forward(&mut engine, "Number(0.000000012).toString(16)") + ); + assert_eq!( + "0.00000034d3fe36aaa0a2", + &forward(&mut engine, "Number(0.0000000123).toString(16)") + ); + + assert_eq!("0", &forward(&mut engine, "Number(-0).toString(16)")); + assert_eq!("-9", &forward(&mut engine, "Number(-9).toString(16)")); + assert_eq!("-5a", &forward(&mut engine, "Number(-90).toString(16)")); + assert_eq!( + "-5a.1eb851eb852", + &forward(&mut engine, "Number(-90.12).toString(16)") + ); + assert_eq!( + "-0.1999999999999a", + &forward(&mut engine, "Number(-0.1).toString(16)") + ); + assert_eq!( + "-0.028f5c28f5c28f6", + &forward(&mut engine, "Number(-0.01).toString(16)") + ); + assert_eq!( + "-0.032617c1bda511a", + &forward(&mut engine, "Number(-0.0123).toString(16)") + ); + assert_eq!( + "-605f9f6dd18bc8000", + &forward(&mut engine, "Number(-111111111111111111111).toString(16)") + ); + assert_eq!( + "-3c3bc3a4a2f75c0000", + &forward(&mut engine, "Number(-1111111111111111111111).toString(16)") + ); + assert_eq!( + "-25a55a46e5da9a00000", + &forward(&mut engine, "Number(-11111111111111111111111).toString(16)") + ); + assert_eq!( + "-0.0000a7c5ac471b4788", + &forward(&mut engine, "Number(-0.00001).toString(16)") + ); + assert_eq!( + "-0.000010c6f7a0b5ed8d", + &forward(&mut engine, "Number(-0.000001).toString(16)") + ); + assert_eq!( + "-0.000001ad7f29abcaf48", + &forward(&mut engine, "Number(-0.0000001).toString(16)") + ); + assert_eq!( + "-0.000002036565348d256", + &forward(&mut engine, "Number(-0.00000012).toString(16)") + ); + assert_eq!( + "-0.0000021047ee22aa466", + &forward(&mut engine, "Number(-0.000000123).toString(16)") + ); + assert_eq!( + "-0.0000002af31dc4611874", + &forward(&mut engine, "Number(-0.00000001).toString(16)") + ); + assert_eq!( + "-0.000000338a23b87483be", + &forward(&mut engine, "Number(-0.000000012).toString(16)") + ); + assert_eq!( + "-0.00000034d3fe36aaa0a2", + &forward(&mut engine, "Number(-0.0000000123).toString(16)") + ); +} + +#[test] +#[ignore] +// This tests fail for now since the Rust's default formatting for exponential format does not match the js spec. +// https://github.com/jasonwilliams/boa/pull/381#discussion_r422458544 +fn num_to_string_exponential() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + + assert_eq!( + String::from("111111111111111110000"), + forward(&mut engine, "Number(111111111111111111111).toString()") + ); + assert_eq!( + String::from("1.1111111111111111e+21"), + forward(&mut engine, "Number(1111111111111111111111).toString()") + ); + assert_eq!( + String::from("1.1111111111111111e+22"), + forward(&mut engine, "Number(11111111111111111111111).toString()") + ); + assert_eq!( + String::from("1e-7"), + forward(&mut engine, "Number(0.0000001).toString()") + ); + assert_eq!( + String::from("1.2e-7"), + forward(&mut engine, "Number(0.00000012).toString()") + ); + assert_eq!( + String::from("1.23e-7"), + forward(&mut engine, "Number(0.000000123).toString()") + ); + assert_eq!( + String::from("1e-8"), + forward(&mut engine, "Number(0.00000001).toString()") + ); + assert_eq!( + String::from("1.2e-8"), + forward(&mut engine, "Number(0.000000012).toString()") + ); + assert_eq!( + String::from("1.23e-8"), + forward(&mut engine, "Number(0.0000000123).toString()") + ); } #[test]