Skip to content

Commit

Permalink
Implement toString (#381)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Tunahan Karlıbaş authored May 10, 2020
1 parent 59df3ac commit bdad99c
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 21 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions boa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
165 changes: 163 additions & 2 deletions boa/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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()`
Expand Down
Loading

0 comments on commit bdad99c

Please sign in to comment.