Browse Source

Implement toString (#381)

* Implement optional parameter `radix` for `Number.prototype.toString( [radix] )

Implement the radix paramet for the `toString`. This implementation is
converted from the V8's c++ implementation.

* Use a reversed iterator instead of cursors in the integer part.

Initial version for getting rid of direct slice accesses. Currently
converted integer part to iterators. Fraction part is a lot harder since
there are two passes to the fraction part (for carry over) and it is
hard to express that using iterators.

* Format tests
pull/391/head
Tunahan Karlıbaş 5 years ago committed by GitHub
parent
commit
bdad99cb82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Cargo.lock
  2. 1
      boa/Cargo.toml
  3. 165
      boa/src/builtins/number/mod.rs
  4. 222
      boa/src/builtins/number/tests.rs

1
Cargo.lock generated

@ -7,6 +7,7 @@ dependencies = [
"criterion", "criterion",
"gc", "gc",
"jemallocator", "jemallocator",
"num-traits",
"rand", "rand",
"regex", "regex",
"rustc-hash", "rustc-hash",

1
boa/Cargo.toml

@ -14,6 +14,7 @@ edition = "2018"
gc = { version = "0.3.4", features = ["derive"] } gc = { version = "0.3.4", features = ["derive"] }
serde_json = "1.0.52" serde_json = "1.0.52"
rand = "0.7.3" rand = "0.7.3"
num-traits = "0.2.11"
regex = "1.3.7" regex = "1.3.7"
rustc-hash = "1.1.0" rustc-hash = "1.1.0"

165
boa/src/builtins/number/mod.rs

@ -23,6 +23,7 @@ use crate::{
}, },
exec::Interpreter, exec::Interpreter,
}; };
use num_traits::float::FloatCore;
use std::{borrow::Borrow, f64, ops::Deref}; use std::{borrow::Borrow, f64, ops::Deref};
/// Helper function that converts a Value to a Number. /// 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"); 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] )` /// `Number.prototype.toString( [radix] )`
/// ///
/// The `toString()` method returns a string representing the specified Number object. /// 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 /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tostring
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/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 { pub fn to_string(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue {
Ok(Value::from(format!("{}", to_number(this).to_number()))) // 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()` /// `Number.prototype.toString()`

222
boa/src/builtins/number/tests.rs

@ -162,26 +162,210 @@ fn to_precision() {
fn to_string() { fn to_string() {
let realm = Realm::create(); let realm = Realm::create();
let mut engine = Executor::new(realm); 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)); assert_eq!("NaN", &forward(&mut engine, "Number(NaN).toString()"));
let default_string = forward(&mut engine, "default_string"); assert_eq!("Infinity", &forward(&mut engine, "Number(1/0).toString()"));
let int_string = forward(&mut engine, "int_string"); assert_eq!(
let float_string = forward(&mut engine, "float_string"); "-Infinity",
let exp_string = forward(&mut engine, "exp_string"); &forward(&mut engine, "Number(-1/0).toString()")
let neg_string = forward(&mut engine, "neg_string"); );
assert_eq!("0", &forward(&mut engine, "Number(0).toString()"));
assert_eq!(default_string, String::from("0")); assert_eq!("9", &forward(&mut engine, "Number(9).toString()"));
assert_eq!(int_string, String::from("123")); assert_eq!("90", &forward(&mut engine, "Number(90).toString()"));
assert_eq!(float_string, String::from("1.234")); assert_eq!("90.12", &forward(&mut engine, "Number(90.12).toString()"));
assert_eq!(exp_string, String::from("12000")); assert_eq!("0.1", &forward(&mut engine, "Number(0.1).toString()"));
assert_eq!(neg_string, String::from("-1.2")); 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] #[test]

Loading…
Cancel
Save