Improve number to string conversions (#400)

integer conversions:
- improve `u32toa_radix` and `u64toa_radix`, add `i32toa_radix`
- use `i32toa_radix` for small ints in `js_number_toString`

floating point conversions (`js_dtoa`):
- complete rewrite with fewer calls to `snprintf`
- remove `JS_DTOA_FORMAT`, define 4 possible modes for `js_dtoa`
- remove the radix argument in `js_dtoa`
- merge `js_dtoa1` into `js_dtoa`
- add `js_dtoa_infinite` for non finite values
- simplify sign handling
- handle locale specific decimal point transparently

helper function `js_fcvt`:
- simplify `js_fcvt`, remove `js_fcvt1`, reduce overhead
- round up manually instead of using `fesetround(FE_UPWARD)`.

helper function `js_ecvt`:
- document `js_ecvt` and `js_ecvt1` behavior
- avoid redundant `js_ecvt1` calls in `js_ecvt`
- fixed buffer contents, no buffer copies
- simplify decimal point handling
- round up manually instead of using `fesetround(FE_UPWARD)`.

miscellaneous:
- remove `CONFIG_PRINTF_RNDN`. This fixes some of the conversion errors
  on Windows. Updated the tests accordingly
- this fixes a v8.sh bug on macOS: `0.5.toFixed(0)` used to produce `0` instead of `1`
- add regression tests, update test_conv unit tests
- add benchmarks for `toFixed`, `toPrecision` and `toExponential` number methods
- benchmarks show all conversions are now 40 to 45% faster (M2)
This commit is contained in:
Charlie Gordon 2024-05-26 08:06:36 +02:00 committed by GitHub
parent 139b51fe4b
commit 9e67b47c0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 474 additions and 325 deletions

View file

@ -575,6 +575,9 @@ overflow:
/* 2 <= base <= 36 */
char const digits36[36] = "0123456789abcdefghijklmnopqrstuvwxyz";
#define USE_SPECIAL_RADIX_10 1 // special case base 10 radix conversions
#define USE_SINGLE_CASE_FAST 1 // special case single digit numbers
/* using u32toa_shift variant */
#define gen_digit(buf, c) if (is_be()) \
@ -613,11 +616,13 @@ size_t u07toa_shift(char dest[minimum_length(8)], uint32_t n, size_t len)
size_t u32toa(char buf[minimum_length(11)], uint32_t n)
{
#ifdef USE_SINGLE_CASE_FAST /* 10% */
if (n < 10) {
buf[0] = (char)('0' + n);
buf[1] = '\0';
return 1;
}
#endif
#define TEN_POW_7 10000000
if (n >= TEN_POW_7) {
uint32_t quo = n / TEN_POW_7;
@ -679,6 +684,8 @@ static uint8_t const radix_shift[64] = {
size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned base)
{
int shift;
#ifdef USE_SPECIAL_RADIX_10
if (likely(base == 10))
return u32toa(buf, n);
@ -688,13 +695,13 @@ size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned base)
buf[1] = '\0';
return 1;
}
int shift = radix_shift[base & 63];
shift = radix_shift[base & 63];
if (shift) {
uint32_t mask = (1 << shift) - 1;
size_t len = (32 - clz32(n) + shift - 1) / shift;
size_t last = n & mask;
n /= base;
char *end = buf + len;
n >>= shift;
*end-- = '\0';
*end-- = digits36[last];
while (n >= base) {
@ -728,11 +735,13 @@ size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned base)
size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned base)
{
int shift;
#ifdef USE_SPECIAL_RADIX_10
if (likely(base == 10))
return u64toa(buf, n);
#endif
int shift = radix_shift[base & 63];
shift = radix_shift[base & 63];
if (shift) {
if (n < base) {
buf[0] = digits36[n];
@ -742,8 +751,8 @@ size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned base)
uint64_t mask = (1 << shift) - 1;
size_t len = (64 - clz64(n) + shift - 1) / shift;
size_t last = n & mask;
n /= base;
char *end = buf + len;
n >>= shift;
*end-- = '\0';
*end-- = digits36[last];
while (n >= base) {
@ -777,6 +786,15 @@ size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned base)
}
}
size_t i32toa_radix(char buf[minimum_length(34)], int32_t n, unsigned int base)
{
if (likely(n >= 0))
return u32toa_radix(buf, n, base);
buf[0] = '-';
return 1 + u32toa_radix(buf + 1, -(uint32_t)n, base);
}
size_t i64toa_radix(char buf[minimum_length(66)], int64_t n, unsigned int base)
{
if (likely(n >= 0))
@ -786,6 +804,11 @@ size_t i64toa_radix(char buf[minimum_length(66)], int64_t n, unsigned int base)
return 1 + u64toa_radix(buf + 1, -(uint64_t)n, base);
}
#undef gen_digit
#undef TEN_POW_7
#undef USE_SPECIAL_RADIX_10
#undef USE_SINGLE_CASE_FAST
/*---- sorting with opaque argument ----*/
typedef void (*exchange_f)(void *a, void *b, size_t size);

View file

@ -463,6 +463,7 @@ size_t i32toa(char buf[minimum_length(12)], int32_t n);
size_t u64toa(char buf[minimum_length(21)], uint64_t n);
size_t i64toa(char buf[minimum_length(22)], int64_t n);
size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned int base);
size_t i32toa_radix(char buf[minimum_length(34)], int32_t n, unsigned base);
size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned int base);
size_t i64toa_radix(char buf[minimum_length(66)], int64_t n, unsigned int base);

628
quickjs.c
View file

@ -58,11 +58,6 @@
#define MALLOC_OVERHEAD 8
#endif
#if !defined(_WIN32) && !defined(__wasi__)
/* define it if printf uses the RNDN rounding mode instead of RNDNA */
#define CONFIG_PRINTF_RNDN
#endif
#if defined(__NEWLIB__)
#define NO_TM_GMTOFF
#endif
@ -10965,262 +10960,354 @@ static JSValue js_bigint_to_string(JSContext *ctx, JSValue val)
return js_bigint_to_string1(ctx, val, 10);
}
/* buf1 contains the printf result */
static void js_ecvt1(double d, int n_digits, int *decpt, int *sign, char *buf,
int rounding_mode, char *buf1, int buf1_size)
/*---- floating point number to string conversions ----*/
/* JavaScript rounding is specified as round to nearest tie away
from zero (RNDNA), but in `printf` the "ties" case is not
specified (in most cases it is RNDN, round to nearest, tie to even),
so we must round manually. We generate 2 extra places and make
an extra call to snprintf if these are exactly '50'.
We set the current rounding mode to FE_DOWNWARD to check if the
last 2 places become '49'. If not, we must round up, which is
performed in place using the string digits.
Note that we cannot rely on snprintf for rounding up:
the code below fails on macOS for `0.5.toFixed(0)`: gives `0` expected `1`
fesetround(FE_UPWARD);
snprintf(dest, size, "%.*f", n_digits, d);
fesetround(FE_TONEAREST);
*/
/* `js_fcvt` minimum buffer length:
- up to 21 digits in integral part
- 1 potential decimal point
- up to 102 decimals
- 1 null terminator
*/
#define JS_FCVT_BUF_SIZE (21+1+102+1)
/* `js_ecvt` minimum buffer length:
- 1 leading digit
- 1 potential decimal point
- up to 102 decimals
- 5 exponent characters (from 'e-324' to 'e+308')
- 1 null terminator
*/
#define JS_ECVT_BUF_SIZE (1+1+102+5+1)
/* `js_dtoa` minimum buffer length:
- 8 byte prefix
- either JS_FCVT_BUF_SIZE or JS_ECVT_BUF_SIZE
- JS_FCVT_BUF_SIZE is larger than JS_ECVT_BUF_SIZE
*/
#define JS_DTOA_BUF_SIZE (8+JS_FCVT_BUF_SIZE)
/* `js_ecvt1`: compute the digits and decimal point spot for a double
- `d` is finite, positive or zero
- `n_digits` number of significant digits in range 1..103
- `buf` receives the printf result
- `buf` has a fixed format: n_digits with a decimal point at offset 1
and exponent 'e{+/-}xx[x]' at offset n_digits+1
Return n_digits
Store the position of the decimal point into `*decpt`
*/
static int js_ecvt1(double d, int n_digits,
char dest[minimum_length(JS_ECVT_BUF_SIZE)],
size_t size, int *decpt)
{
if (rounding_mode != FE_TONEAREST)
fesetround(rounding_mode);
snprintf(buf1, buf1_size, "%+.*e", n_digits - 1, d);
if (rounding_mode != FE_TONEAREST)
fesetround(FE_TONEAREST);
*sign = (buf1[0] == '-');
/* mantissa */
buf[0] = buf1[1];
if (n_digits > 1)
memcpy(buf + 1, buf1 + 3, n_digits - 1);
buf[n_digits] = '\0';
/* exponent */
*decpt = atoi(buf1 + n_digits + 2 + (n_digits > 1)) + 1;
}
/* maximum buffer size for js_dtoa */
#define JS_DTOA_BUF_SIZE 128
/* needed because ecvt usually limits the number of digits to
17. Return the number of digits. */
static int js_ecvt(double d, int n_digits, int *decpt, int *sign, char *buf,
BOOL is_fixed)
{
int rounding_mode;
char buf_tmp[JS_DTOA_BUF_SIZE];
if (!is_fixed) {
unsigned int n_digits_min, n_digits_max;
/* find the minimum amount of digits (XXX: inefficient but simple) */
n_digits_min = 1;
n_digits_max = 17;
while (n_digits_min < n_digits_max) {
n_digits = (n_digits_min + n_digits_max) / 2;
js_ecvt1(d, n_digits, decpt, sign, buf, FE_TONEAREST,
buf_tmp, sizeof(buf_tmp));
if (strtod(buf_tmp, NULL) == d) {
/* no need to keep the trailing zeros */
while (n_digits >= 2 && buf[n_digits - 1] == '0')
n_digits--;
n_digits_max = n_digits;
} else {
n_digits_min = n_digits + 1;
}
}
n_digits = n_digits_max;
rounding_mode = FE_TONEAREST;
} else {
rounding_mode = FE_TONEAREST;
#ifdef CONFIG_PRINTF_RNDN
{
char buf1[JS_DTOA_BUF_SIZE], buf2[JS_DTOA_BUF_SIZE];
int decpt1, sign1, decpt2, sign2;
/* The JS rounding is specified as round to nearest ties away
from zero (RNDNA), but in printf the "ties" case is not
specified (for example it is RNDN for glibc, RNDNA for
Windows), so we must round manually. */
js_ecvt1(d, n_digits + 1, &decpt1, &sign1, buf1, FE_TONEAREST,
buf_tmp, sizeof(buf_tmp));
/* XXX: could use 2 digits to reduce the average running time */
if (buf1[n_digits] == '5') {
js_ecvt1(d, n_digits + 1, &decpt1, &sign1, buf1, FE_DOWNWARD,
buf_tmp, sizeof(buf_tmp));
js_ecvt1(d, n_digits + 1, &decpt2, &sign2, buf2, FE_UPWARD,
buf_tmp, sizeof(buf_tmp));
if (memcmp(buf1, buf2, n_digits + 1) == 0 && decpt1 == decpt2) {
/* exact result: round away from zero */
if (sign1)
rounding_mode = FE_DOWNWARD;
else
rounding_mode = FE_UPWARD;
}
}
}
#endif /* CONFIG_PRINTF_RNDN */
}
js_ecvt1(d, n_digits, decpt, sign, buf, rounding_mode,
buf_tmp, sizeof(buf_tmp));
/* d is positive, ensure decimal point is always present */
snprintf(dest, size, "%#.*e", n_digits - 1, d);
/* dest contents:
0: first digit
1: '.' decimal point (locale specific)
2..n_digits: (n_digits-1) additional digits
n_digits+1: 'e' exponent mark
n_digits+2..: exponent sign, value and null terminator
*/
/* extract the exponent (actually the position of the decimal point) */
*decpt = 1 + atoi(dest + n_digits + 2);
return n_digits;
}
static size_t js_fcvt1(char (*buf)[JS_DTOA_BUF_SIZE], double d, int n_digits,
int rounding_mode)
/* `js_ecvt`: compute the digits and decimal point spot for a double
with proper javascript rounding. We cannot use `ecvt` for multiple
resasons: portability, because of the number of digits is typically
limited to 17, finally because the default rounding is inadequate.
`d` is finite and positive or zero.
`n_digits` number of significant digits in range 1..101
or 0 for automatic (only as many digits as necessary)
Return the number of digits produced in `dest`.
Store the position of the decimal point into `*decpt`
*/
static int js_ecvt(double d, int n_digits,
char dest[minimum_length(JS_ECVT_BUF_SIZE)],
size_t size, int *decpt)
{
size_t n;
if (rounding_mode != FE_TONEAREST)
fesetround(rounding_mode);
n = snprintf(*buf, sizeof(*buf), "%.*f", n_digits, d);
if (rounding_mode != FE_TONEAREST)
fesetround(FE_TONEAREST);
assert(n < sizeof(*buf));
return n;
}
int i;
static size_t js_fcvt(char (*buf)[JS_DTOA_BUF_SIZE], double d, int n_digits)
{
int rounding_mode;
rounding_mode = FE_TONEAREST;
#ifdef CONFIG_PRINTF_RNDN
{
int n1, n2;
char buf1[JS_DTOA_BUF_SIZE];
char buf2[JS_DTOA_BUF_SIZE];
/* The JS rounding is specified as round to nearest ties away from
zero (RNDNA), but in printf the "ties" case is not specified
(for example it is RNDN for glibc, RNDNA for Windows), so we
must round manually. */
n1 = js_fcvt1(&buf1, d, n_digits + 1, FE_TONEAREST);
rounding_mode = FE_TONEAREST;
/* XXX: could use 2 digits to reduce the average running time */
if (buf1[n1 - 1] == '5') {
n1 = js_fcvt1(&buf1, d, n_digits + 1, FE_DOWNWARD);
n2 = js_fcvt1(&buf2, d, n_digits + 1, FE_UPWARD);
if (n1 == n2 && memcmp(buf1, buf2, n1) == 0) {
/* exact result: round away from zero */
if (buf1[0] == '-')
rounding_mode = FE_DOWNWARD;
else
rounding_mode = FE_UPWARD;
if (n_digits == 0) {
/* find the minimum number of digits (XXX: inefficient but simple) */
// TODO(chqrlie) use direct method from quickjs-printf
unsigned int n_digits_min = 1;
unsigned int n_digits_max = 17;
for (;;) {
n_digits = (n_digits_min + n_digits_max) / 2;
js_ecvt1(d, n_digits, dest, size, decpt);
if (n_digits_min == n_digits_max)
return n_digits;
/* dest contents:
0: first digit
1: '.' decimal point (locale specific)
2..n_digits: (n_digits-1) additional digits
n_digits+1: 'e' exponent mark
n_digits+2..: exponent sign, value and null terminator
*/
if (strtod(dest, NULL) == d) {
unsigned int n0 = n_digits;
/* enough digits */
/* strip the trailing zeros */
while (dest[n_digits] == '0')
n_digits--;
if (n_digits == n_digits_min)
return n_digits;
/* done if trailing zeros and not denormal or huge */
if (n_digits < n0 && d > 3e-308 && d < 8e307)
return n_digits;
n_digits_max = n_digits;
} else {
/* need at least one more digit */
n_digits_min = n_digits + 1;
}
}
}
#endif /* CONFIG_PRINTF_RNDN */
return js_fcvt1(buf, d, n_digits, rounding_mode);
}
/* radix != 10 is only supported with flags = JS_DTOA_VAR_FORMAT */
/* use as many digits as necessary */
#define JS_DTOA_VAR_FORMAT (0 << 0)
/* use n_digits significant digits (1 <= n_digits <= 101) */
#define JS_DTOA_FIXED_FORMAT (1 << 0)
/* force fractional format: [-]dd.dd with n_digits fractional digits */
#define JS_DTOA_FRAC_FORMAT (2 << 0)
/* force exponential notation either in fixed or variable format */
#define JS_DTOA_FORCE_EXP (1 << 2)
/* XXX: slow and maybe not fully correct. Use libbf when it is fast enough.
XXX: radix != 10 is only supported for small integers
*/
static size_t js_dtoa1(char (*buf)[JS_DTOA_BUF_SIZE], double d,
int radix, int n_digits, int flags)
{
char *q;
if (!isfinite(d)) {
if (isnan(d)) {
memcpy(*buf, "NaN", sizeof "NaN");
return sizeof("NaN") - 1;
} else if (d < 0) {
memcpy(*buf, "-Infinity", sizeof "-Infinity");
return sizeof("-Infinity") - 1;
} else {
memcpy(*buf, "Infinity", sizeof "Infinity");
return sizeof("Infinity") - 1;
}
} else if (flags == JS_DTOA_VAR_FORMAT) {
int64_t i64;
char buf1[72], *ptr;
if (d > (double)MAX_SAFE_INTEGER || d < (double)-MAX_SAFE_INTEGER)
goto generic_conv;
i64 = (int64_t)d;
if (d != i64)
goto generic_conv;
/* fast path for integers */
return i64toa_radix(*buf, i64, radix);
} else {
if (d == 0.0)
d = 0.0; /* convert -0 to 0 */
if (flags == JS_DTOA_FRAC_FORMAT) {
return js_fcvt(buf, d, n_digits);
} else {
char buf1[JS_DTOA_BUF_SIZE];
int sign, decpt, k, n, i, p, n_max;
BOOL is_fixed;
generic_conv:
is_fixed = ((flags & 3) == JS_DTOA_FIXED_FORMAT);
if (is_fixed) {
n_max = n_digits;
} else {
n_max = 21;
}
/* the number has k digits (k >= 1) */
k = js_ecvt(d, n_digits, &decpt, &sign, buf1, is_fixed);
n = decpt; /* d=10^(n-k)*(buf1) i.e. d= < x.yyyy 10^(n-1) */
q = *buf;
if (sign)
*q++ = '-';
if (flags & JS_DTOA_FORCE_EXP)
goto force_exp;
if (n >= 1 && n <= n_max) {
if (k <= n) {
memcpy(q, buf1, k);
q += k;
for(i = 0; i < (n - k); i++)
*q++ = '0';
*q = '\0';
} else {
/* k > n */
memcpy(q, buf1, n);
q += n;
*q++ = '.';
for(i = 0; i < (k - n); i++)
*q++ = buf1[n + i];
*q = '\0';
}
} else if (n >= -5 && n <= 0) {
*q++ = '0';
*q++ = '.';
for(i = 0; i < -n; i++)
*q++ = '0';
memcpy(q, buf1, k);
q += k;
*q = '\0';
} else {
force_exp:
/* exponential notation */
*q++ = buf1[0];
if (k > 1) {
*q++ = '.';
for(i = 1; i < k; i++)
*q++ = buf1[i];
}
*q++ = 'e';
p = n - 1;
if (p >= 0)
*q++ = '+';
q += snprintf(q, *buf + sizeof(*buf) - q, "%d", p);
}
return q - *buf;
#if defined(FE_DOWNWARD) && defined(FE_TONEAREST)
/* generate 2 extra digits: 99% chances to avoid 2 calls */
js_ecvt1(d, n_digits + 2, dest, size, decpt);
if (dest[n_digits + 1] < '5')
return n_digits; /* truncate the 2 extra digits */
if (dest[n_digits + 1] == '5' && dest[n_digits + 2] == '0') {
/* close to half-way: try rounding toward 0 */
fesetround(FE_DOWNWARD);
js_ecvt1(d, n_digits + 2, dest, size, decpt);
fesetround(FE_TONEAREST);
if (dest[n_digits + 1] < '5')
return n_digits; /* truncate the 2 extra digits */
}
/* round up in the string */
for(i = n_digits;; i--) {
/* ignore the locale specific decimal point */
if (is_digit(dest[i])) {
if (dest[i]++ < '9')
break;
dest[i] = '0';
if (i == 0) {
dest[0] = '1';
(*decpt)++;
break;
}
}
}
return n_digits; /* truncate the 2 extra digits */
#else
/* No disambiguation available, eg: __wasi__ targets */
return js_ecvt1(d, n_digits, dest, size, decpt);
#endif
}
}
static JSValue js_dtoa(JSContext *ctx,
double d, int radix, int n_digits, int flags)
/* `js_fcvt`: convert a floating point value to %f format using RNDNA
`d` is finite and positive or zero.
`n_digits` number of decimal places in range 0..100
Return the number of characters produced in `dest`.
*/
static size_t js_fcvt(double d, int n_digits,
char dest[minimum_length(JS_FCVT_BUF_SIZE)], size_t size)
{
#if defined(FE_DOWNWARD) && defined(FE_TONEAREST)
int i, n1, n2;
/* generate 2 extra digits: 99% chances to avoid 2 calls */
n1 = snprintf(dest, size, "%.*f", n_digits + 2, d) - 2;
if (dest[n1] >= '5') {
if (dest[n1] == '5' && dest[n1 + 1] == '0') {
/* close to half-way: try rounding toward 0 */
fesetround(FE_DOWNWARD);
n1 = snprintf(dest, size, "%.*f", n_digits + 2, d) - 2;
fesetround(FE_TONEAREST);
}
if (dest[n1] >= '5') { /* number should be rounded up */
/* d is either exactly half way or greater: round the string manually */
for (i = n1 - 1;; i--) {
/* ignore the locale specific decimal point */
if (is_digit(dest[i])) {
if (dest[i]++ < '9')
break;
dest[i] = '0';
if (i == 0) {
dest[0] = '1';
dest[n1] = '0';
dest[n1 - n_digits - 1] = '0';
dest[n1 - n_digits] = '.';
n1++;
break;
}
}
}
}
}
/* truncate the extra 2 digits and the decimal point if !n_digits */
n1 -= !n_digits;
//dest[n1] = '\0'; // optional
return n1;
#else
/* No disambiguation available, eg: __wasi__ targets */
return snprintf(dest, size, "%.*f", n_digits, d);
#endif
}
static JSValue js_dtoa_infinite(JSContext *ctx, double d)
{
// TODO(chqrlie) use atoms for NaN and Infinite?
if (isnan(d))
return js_new_string8(ctx, "NaN");
if (d < 0)
return js_new_string8(ctx, "-Infinity");
else
return js_new_string8(ctx, "Infinity");
}
#define JS_DTOA_TOSTRING 0 /* use as many digits as necessary */
#define JS_DTOA_EXPONENTIAL 1 /* use exponential notation either fixed or variable digits */
#define JS_DTOA_FIXED 2 /* force fixed number of fractional digits */
#define JS_DTOA_PRECISION 3 /* use n_digits significant digits (1 <= n_digits <= 101) */
/* `js_dtoa`: convert a floating point number to a string
- `mode`: one of the 4 supported formats
- `n_digits`: digit number according to mode
- TOSTRING: 0 only. As many digits as necessary
- EXPONENTIAL: 0 as many decimals as necessary
- 1..101 number of significant digits
- FIXED: 0..100 number of decimal places
- PRECISION: 1..101 number of significant digits
*/
// XXX: should use libbf or quickjs-printf.
static JSValue js_dtoa(JSContext *ctx, double d, int n_digits, int mode)
{
char buf[JS_DTOA_BUF_SIZE];
size_t len = js_dtoa1(&buf, d, radix, n_digits, flags);
return js_new_string8_len(ctx, buf, len);
size_t len;
char *start;
int sign, decpt, exp, i, k, n, n_max;
if (!isfinite(d))
return js_dtoa_infinite(ctx, d);
sign = (d < 0);
start = buf + 8;
d = fabs(d); /* also converts -0 to 0 */
if (mode != JS_DTOA_EXPONENTIAL && n_digits == 0) {
/* fast path for exact integers in variable format:
clip to MAX_SAFE_INTEGER because to ensure insignificant
digits are generated as 0.
used for JS_DTOA_TOSTRING and JS_DTOA_FIXED without decimals.
*/
if (d <= (double)MAX_SAFE_INTEGER) {
uint64_t u64 = (uint64_t)d;
if (d == u64) {
len = u64toa(start, u64);
goto done;
}
}
}
if (mode == JS_DTOA_FIXED) {
len = js_fcvt(d, n_digits, start, sizeof(buf) - 8);
// TODO(chqrlie) patch the locale specific decimal point
goto done;
}
n_max = (n_digits > 0) ? n_digits : 21;
/* the number has k digits (1 <= k <= n_max) */
k = js_ecvt(d, n_digits, start, sizeof(buf) - 8, &decpt);
/* buffer contents:
0: first digit
1: '.' decimal point
2..k: (k-1) additional digits
*/
n = decpt; /* d=10^(n-k)*(buf1) i.e. d= < x.yyyy 10^(n-1) */
if (mode != JS_DTOA_EXPONENTIAL) {
/* mode is JS_DTOA_PRECISION or JS_DTOA_TOSTRING */
if (n >= 1 && n <= n_max) {
/* between 1 and n_max digits before the decimal point */
if (k <= n) {
/* all digits before the point, append zeros */
start[1] = start[0];
start++;
for(i = k; i < n; i++)
start[i] = '0';
len = n;
} else {
/* k > n: move digits before the point */
for(i = 1; i < n; i++)
start[i] = start[i + 1];
start[i] = '.';
len = 1 + k;
}
goto done;
}
if (n >= -5 && n <= 0) {
/* insert -n leading 0 decimals and a '0.' prefix */
n = -n;
start[1] = start[0];
start -= n + 1;
start[0] = '0';
start[1] = '.';
for(i = 0; i < n; i++)
start[2 + i] = '0';
len = 2 + k + n;
goto done;
}
}
/* exponential notation */
exp = n - 1;
/* count the digits and the decimal point if at least one decimal */
len = k + (k > 1);
start[1] = '.'; /* patch the locale specific decimal point */
start[len] = 'e';
start[len + 1] = '+';
if (exp < 0) {
start[len + 1] = '-';
exp = -exp;
}
len += 2 + 1 + (exp > 9) + (exp > 99);
for (i = len - 1; exp > 9;) {
int quo = exp / 10;
start[i--] = (char)('0' + exp % 10);
exp = quo;
}
start[i] = (char)('0' + exp);
done:
start[-1] = '-'; /* prepend the sign if negative */
return js_new_string8_len(ctx, start - sign, len + sign);
}
/* d is guaranteed to be finite */
/* `js_dtoa_radix`: convert a floating point number using a specific base
- `d` must be finite
- `radix` must be in range 2..36
*/
static JSValue js_dtoa_radix(JSContext *ctx, double d, int radix)
{
char buf[2200], *ptr, *ptr2, *ptr3;
/* d is finite */
int sign = d < 0;
int digit;
int sign, digit;
double frac, d0;
int64_t n0 = 0;
int64_t n0;
if (!isfinite(d))
return js_dtoa_infinite(ctx, d);
sign = (d < 0);
d = fabs(d);
d0 = trunc(d);
n0 = 0;
frac = d - d0;
ptr2 = buf + 1100; /* ptr2 points to the end of the string */
ptr = ptr2; /* ptr points to the beginning of the string */
@ -11284,7 +11371,7 @@ static JSValue js_dtoa_radix(JSContext *ctx, double d, int radix)
ptr2[-1] = (ptr2[-1] == '9') ? 'a' : ptr2[-1] + 1;
}
} else {
/* strip trailing fractional zeroes */
/* strip trailing fractional zeros */
while (ptr2[-1] == '0')
ptr2--;
/* strip the 'decimal' point if last */
@ -11340,8 +11427,7 @@ JSValue JS_ToStringInternal(JSContext *ctx, JSValue val, BOOL is_ToPropertyKey)
return JS_ThrowTypeError(ctx, "cannot convert symbol to string");
}
case JS_TAG_FLOAT64:
return js_dtoa(ctx, JS_VALUE_GET_FLOAT64(val), 10, 0,
JS_DTOA_VAR_FORMAT);
return js_dtoa(ctx, JS_VALUE_GET_FLOAT64(val), 0, JS_DTOA_TOSTRING);
case JS_TAG_BIG_INT:
return js_bigint_to_string(ctx, val);
default:
@ -38961,7 +39047,7 @@ static int js_get_radix(JSContext *ctx, JSValue val)
if (JS_ToInt32Sat(ctx, &radix, val))
return -1;
if (radix < 2 || radix > 36) {
JS_ThrowRangeError(ctx, "radix must be between 2 and 36");
JS_ThrowRangeError(ctx, "toString() radix argument must be between 2 and 36");
return -1;
}
return radix;
@ -38986,22 +39072,21 @@ static JSValue js_number_toString(JSContext *ctx, JSValue this_val,
base = 10;
} else {
base = js_get_radix(ctx, argv[0]);
if (base < 0)
goto fail;
if (base < 0) {
JS_FreeValue(ctx, val);
return JS_EXCEPTION;
}
}
if (JS_VALUE_GET_TAG(val) == JS_TAG_INT) {
size_t len = i64toa_radix(buf, JS_VALUE_GET_INT(val), base);
size_t len = i32toa_radix(buf, JS_VALUE_GET_INT(val), base);
return js_new_string8_len(ctx, buf, len);
}
if (JS_ToFloat64Free(ctx, &d, val))
return JS_EXCEPTION;
if (base != 10 && isfinite(d)) {
if (base != 10)
return js_dtoa_radix(ctx, d, base);
}
return js_dtoa(ctx, d, base, 0, JS_DTOA_VAR_FORMAT);
fail:
JS_FreeValue(ctx, val);
return JS_EXCEPTION;
return js_dtoa(ctx, d, 0, JS_DTOA_TOSTRING);
}
static JSValue js_number_toFixed(JSContext *ctx, JSValue this_val,
@ -39019,13 +39104,13 @@ static JSValue js_number_toFixed(JSContext *ctx, JSValue this_val,
if (JS_ToInt32Sat(ctx, &f, argv[0]))
return JS_EXCEPTION;
if (f < 0 || f > 100) {
return JS_ThrowRangeError(ctx, "%s() argument must be between 1 and 100",
"toFixed");
return JS_ThrowRangeError(ctx, "toFixed() digits argument must be between 0 and 100");
}
if (fabs(d) >= 1e21) {
return JS_ToStringFree(ctx, js_float64(d));
// use ToString(d)
return js_dtoa(ctx, d, 0, JS_DTOA_TOSTRING);
} else {
return js_dtoa(ctx, d, 10, f, JS_DTOA_FRAC_FORMAT);
return js_dtoa(ctx, d, f, JS_DTOA_FIXED);
}
}
@ -39033,8 +39118,8 @@ static JSValue js_number_toExponential(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv)
{
JSValue val;
int f, flags;
double d;
int f;
val = js_thisNumberValue(ctx, this_val);
if (JS_IsException(val))
@ -39043,21 +39128,15 @@ static JSValue js_number_toExponential(JSContext *ctx, JSValue this_val,
return JS_EXCEPTION;
if (JS_ToInt32Sat(ctx, &f, argv[0]))
return JS_EXCEPTION;
if (!isfinite(d)) {
return JS_ToStringFree(ctx, js_float64(d));
}
if (JS_IsUndefined(argv[0])) {
flags = 0;
f = 0;
} else {
if (!isfinite(d))
return js_dtoa_infinite(ctx, d);
if (!JS_IsUndefined(argv[0])) {
if (f < 0 || f > 100) {
return JS_ThrowRangeError(ctx, "%s() argument must be between 1 and 100",
"toExponential");
return JS_ThrowRangeError(ctx, "toExponential() argument must be between 0 and 100");
}
f++;
flags = JS_DTOA_FIXED_FORMAT;
f += 1; /* number of significant digits between 1 and 101 */
}
return js_dtoa(ctx, d, 10, f, flags | JS_DTOA_FORCE_EXP);
return js_dtoa(ctx, d, f, JS_DTOA_EXPONENTIAL);
}
static JSValue js_number_toPrecision(JSContext *ctx, JSValue this_val,
@ -39073,18 +39152,15 @@ static JSValue js_number_toPrecision(JSContext *ctx, JSValue this_val,
if (JS_ToFloat64Free(ctx, &d, val))
return JS_EXCEPTION;
if (JS_IsUndefined(argv[0]))
goto to_string;
return js_dtoa(ctx, d, 0, JS_DTOA_TOSTRING);
if (JS_ToInt32Sat(ctx, &p, argv[0]))
return JS_EXCEPTION;
if (!isfinite(d)) {
to_string:
return JS_ToStringFree(ctx, js_float64(d));
}
if (!isfinite(d))
return js_dtoa_infinite(ctx, d);
if (p < 1 || p > 100) {
return JS_ThrowRangeError(ctx, "%s() argument must be between 1 and 100",
"toPrecision");
return JS_ThrowRangeError(ctx, "toPrecision() argument must be between 1 and 100");
}
return js_dtoa(ctx, d, 10, p, JS_DTOA_FIXED_FORMAT);
return js_dtoa(ctx, d, p, JS_DTOA_PRECISION);
}
static const JSCFunctionListEntry js_number_proto_funcs[] = {

View file

@ -919,6 +919,42 @@ function float_toString(n)
return n * 3;
}
function float_toFixed(n)
{
var s, r, j;
r = 0;
for(j = 0; j < n; j++) {
s = (j % 10 + 0.1).toFixed(j % 16);
s = (j + 0.1).toFixed(j % 16);
s = (j * 12345678 + 0.1).toFixed(j % 16);
}
return n * 3;
}
function float_toPrecision(n)
{
var s, r, j;
r = 0;
for(j = 0; j < n; j++) {
s = (j % 10 + 0.1).toPrecision(j % 16 + 1);
s = (j + 0.1).toPrecision(j % 16 + 1);
s = (j * 12345678 + 0.1).toPrecision(j % 16 + 1);
}
return n * 3;
}
function float_toExponential(n)
{
var s, r, j;
r = 0;
for(j = 0; j < n; j++) {
s = (j % 10 + 0.1).toExponential(j % 16);
s = (j + 0.1).toExponential(j % 16);
s = (j * 12345678 + 0.1).toExponential(j % 16);
}
return n * 3;
}
function string_to_int(n)
{
var s, r, j;
@ -1014,11 +1050,14 @@ function main(argc, argv, g)
int_toString,
float_to_string,
float_toString,
float_toFixed,
float_toPrecision,
float_toExponential,
string_to_int,
string_to_float,
];
var tests = [];
var i, j, n, f, name;
var i, j, n, f, name, found;
if (typeof BigInt == "function") {
/* BigInt test */
@ -1045,14 +1084,14 @@ function main(argc, argv, g)
sort_bench.array_size = +argv[i++];
continue;
}
for (j = 0; j < test_list.length; j++) {
for (j = 0, found = false; j < test_list.length; j++) {
f = test_list[j];
if (name === f.name) {
if (f.name.startsWith(name)) {
tests.push(f);
break;
found = true;
}
}
if (j == test_list.length) {
if (!found) {
console.log("unknown benchmark: " + name);
return 1;
}
@ -1080,6 +1119,9 @@ function main(argc, argv, g)
save_result("microbench-new.txt", log_data);
}
if (!scriptArgs)
if (typeof scriptArgs === "undefined") {
scriptArgs = [];
if (typeof process.argv === "object")
scriptArgs = process.argv.slice(1);
}
main(scriptArgs.length, scriptArgs, this);

View file

@ -434,18 +434,17 @@ function test_number()
assert(Number.isNaN(Number("-")));
assert(Number.isNaN(Number("\x00a")));
// TODO: Fix rounding errors on Windows/Cygwin.
if (['win32', 'cygwin'].includes(os.platform)) {
return;
}
assert((1-2**-53).toString(12), "0.bbbbbbbbbbbbbba");
assert((1000000000000000128).toString(), "1000000000000000100");
assert((1000000000000000128).toFixed(0), "1000000000000000128");
assert((25).toExponential(0), "3e+1");
assert((-25).toExponential(0), "-3e+1");
assert((2.5).toPrecision(1), "3");
assert((-2.5).toPrecision(1), "-3");
assert((1.125).toFixed(2), "1.13");
assert((-1.125).toFixed(2), "-1.13");
assert((0.5).toFixed(0), "1");
assert((-0.5).toFixed(0), "-1");
}
function test_eval2()

View file

@ -864,6 +864,8 @@ static uint8_t const radix_shift[64] = {
size_t u32toa_radix_length(char buf[minimum_length(33)], uint32_t n, unsigned base)
{
int shift;
#ifdef USE_SPECIAL_RADIX_10
if (likely(base == 10))
return u32toa_length_loop(buf, n);
@ -873,13 +875,13 @@ size_t u32toa_radix_length(char buf[minimum_length(33)], uint32_t n, unsigned ba
buf[1] = '\0';
return 1;
}
int shift = radix_shift[base & 63];
shift = radix_shift[base & 63];
if (shift) {
uint32_t mask = (1 << shift) - 1;
size_t len = (32 - clz32(n) + shift - 1) / shift;
size_t last = n & mask;
n /= base;
char *end = buf + len;
n >>= shift;
*end-- = '\0';
*end-- = digits36[last];
while (n >= base) {
@ -913,11 +915,13 @@ size_t u32toa_radix_length(char buf[minimum_length(33)], uint32_t n, unsigned ba
size_t u64toa_radix_length(char buf[minimum_length(65)], uint64_t n, unsigned base)
{
int shift;
#ifdef USE_SPECIAL_RADIX_10
if (likely(base == 10))
return u64toa_length_loop(buf, n);
#endif
int shift = radix_shift[base & 63];
shift = radix_shift[base & 63];
if (shift) {
if (n < base) {
buf[0] = digits36[n];
@ -927,8 +931,8 @@ size_t u64toa_radix_length(char buf[minimum_length(65)], uint64_t n, unsigned ba
uint64_t mask = (1 << shift) - 1;
size_t len = (64 - clz64(n) + shift - 1) / shift;
size_t last = n & mask;
n /= base;
char *end = buf + len;
n >>= shift;
*end-- = '\0';
*end-- = digits36[last];
while (n >= base) {
@ -1511,6 +1515,9 @@ int main(int argc, char *argv[]) {
clock_t times1[countof(impl1)][4][37];
char buf[100];
uint64_t bases = 0;
#define set_base(bases, b) (*(bases) |= (1ULL << (b)))
#define has_base(bases, b) ((bases) & (1ULL << (b)))
#define single_base(bases) (!((bases) & ((bases) - 1)))
int verbose = 0;
int average = 1;
int enabled = 3;
@ -1521,14 +1528,13 @@ int main(int argc, char *argv[]) {
for (int a = 1; a < argc; a++) {
char *arg = argv[a];
if (isdigit((unsigned char)*arg)) {
verbose = 1;
while (isdigit((unsigned char)*arg)) {
int b1 = strtol(arg, &arg, 10);
bases |= (1ULL << b1);
set_base(&bases, b1);
if (*arg == '-') {
int b2 = strtol(arg, &arg, 10);
int b2 = strtol(arg + 1, &arg, 10);
while (++b1 <= b2)
bases |= (1ULL << b1);
set_base(&bases, b1);
}
if (*arg == ',') {
arg++;
@ -1539,10 +1545,6 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "invalid option syntax: %s\n", argv[a]);
return 2;
}
if (!(bases & (bases - 1))) { /* single base */
average = 0;
verbose = 1;
}
continue;
} else if (!strcmp(arg, "-t") || !strcmp(arg, "--terse")) {
verbose = 0;
@ -1578,14 +1580,19 @@ int main(int argc, char *argv[]) {
}
if (!bases)
bases = -1;
if (bases & (bases - 1)) /* multiple bases */
if (single_base(bases)) {
average = 0;
verbose = 1;
} else {
average = 1;
}
int numvariant = 0;
int numvariant1 = 0;
int nerrors = 0;
if (bases & (1ULL << 10)) {
/* Checking for correctness */
if (has_base(bases, 10)) {
for (size_t i = 0; i < countof(impl); i++) {
unsigned base = 10;
if (impl[i].enabled & enabled) {
@ -1599,7 +1606,7 @@ int main(int argc, char *argv[]) {
for (size_t i = 0; i < countof(impl1); i++) {
if (impl1[i].enabled & enabled) {
for (unsigned base = 2; base <= 36; base++) {
if (bases & (1ULL << base)) {
if (has_base(bases, base)) {
CHECK(impl1[i], 1000, 0, 32, u32toa_radix(buf, x, base), strtoull(buf, NULL, base));
CHECK(impl1[i], 1000, 1, 32, i32toa_radix(buf, x, base), strtoll(buf, NULL, base));
CHECK(impl1[i], 1000, 0, 64, u64toa_radix(buf, x, base), strtoull(buf, NULL, base));
@ -1611,13 +1618,14 @@ int main(int argc, char *argv[]) {
if (nerrors)
return 1;
if (bases & (1ULL << 10)) {
/* Timing conversions */
if (has_base(bases, 10)) {
for (int rep = 0; rep < 100; rep++) {
for (size_t i = 0; i < countof(impl); i++) {
if (impl[i].enabled & enabled) {
numvariant++;
#ifdef TEST_SNPRINTF
if (strstr(impl[i].name, "snprintf")) { // avoid function call overhead
if (strstr(impl[i].name, "snprintf")) { // avoid wrapper overhead
TIME(times[i][0], 1000, 0, 32, snprintf(buf, 11, "%"PRIu32, x));
TIME(times[i][1], 1000, 1, 32, snprintf(buf, 12, "%"PRIi32, x));
TIME(times[i][2], 1000, 0, 64, snprintf(buf, 21, "%"PRIu64, x));
@ -1639,42 +1647,42 @@ int main(int argc, char *argv[]) {
if (impl1[i].enabled & enabled) {
numvariant1++;
#ifdef TEST_SNPRINTF
if (strstr(impl[i].name, "snprintf")) { // avoid function call overhead
if (strstr(impl[i].name, "snprintf")) { // avoid wrapper overhead
#ifdef PRIb32
if (bases & (1ULL << 1)) {
if (has_base(bases, 2)) {
unsigned base = 2;
TIME(times1[i][0][2], 1000, 0, 32, snprintf(buf, 33, "%"PRIb32, x));
TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIb32, x));
TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base));
TIME(times1[i][2][2], 1000, 0, 64, snprintf(buf, 65, "%"PRIb64, x));
TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 65, "%"PRIb64, x));
TIME(times1[i][3][base], 1000, 1, 64, impl1[i].i64toa_radix(buf, x, base));
}
#endif
if (bases & (1ULL << 8)) {
if (has_base(bases, 8)) {
unsigned base = 8;
TIME(times1[i][0][8], 1000, 0, 32, snprintf(buf, 33, "%"PRIo32, x));
TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIo32, x));
TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base));
TIME(times1[i][2][8], 1000, 0, 64, snprintf(buf, 65, "%"PRIo64, x));
TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 65, "%"PRIo64, x));
TIME(times1[i][3][base], 1000, 1, 64, impl1[i].i64toa_radix(buf, x, base));
}
if (bases & (1ULL << 10)) {
if (has_base(bases, 10)) {
unsigned base = 10;
TIME(times1[i][0][10], 1000, 0, 32, snprintf(buf, 33, "%"PRIu32, x));
TIME(times1[i][1][10], 1000, 1, 32, snprintf(buf, 34, "%"PRIi32, x));
TIME(times1[i][2][10], 1000, 0, 64, snprintf(buf, 64, "%"PRIu64, x));
TIME(times1[i][3][10], 1000, 1, 64, snprintf(buf, 65, "%"PRIi64, x));
TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIu32, x));
TIME(times1[i][1][base], 1000, 1, 32, snprintf(buf, 34, "%"PRIi32, x));
TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 64, "%"PRIu64, x));
TIME(times1[i][3][base], 1000, 1, 64, snprintf(buf, 65, "%"PRIi64, x));
}
if (bases & (1ULL << 16)) {
if (has_base(bases, 16)) {
unsigned base = 16;
TIME(times1[i][0][16], 1000, 0, 32, snprintf(buf, 33, "%"PRIx32, x));
TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIx32, x));
TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base));
TIME(times1[i][2][16], 1000, 0, 64, snprintf(buf, 65, "%"PRIx64, x));
TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 65, "%"PRIx64, x));
TIME(times1[i][3][base], 1000, 1, 64, impl1[i].i64toa_radix(buf, x, base));
}
} else
#endif
{
for (unsigned base = 2; base <= 36; base++) {
if (bases & (1ULL << base)) {
if (has_base(bases, base)) {
TIME(times1[i][0][base], 1000, 0, 32, impl1[i].u32toa_radix(buf, x, base));
TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base));
TIME(times1[i][2][base], 1000, 0, 64, impl1[i].u64toa_radix(buf, x, base));
@ -1704,7 +1712,7 @@ int main(int argc, char *argv[]) {
for (size_t i = 0; i < countof(impl1); i++) {
int numbases = 0;
for (unsigned base = 2; base <= 36; base++) {
if (bases & (1ULL << base)) {
if (has_base(bases, base)) {
if (times1[i][0][base]) {
numbases++;
for (int j = 0; j < 4; j++)