Fast paths for number<->string conversion (Number/parseFloat, toFixed, two-decimal String)

String to number: Number(str), unary +, and parseFloat now convert short
decimal spellings with Clinger's exact fast path (scr_decimal_scan in
scr_number.c): significant digits m <= 2^53 and a decimal exponent within
+-22 (plus the folded range up to 22 + 15) need one correctly rounded
multiply or divide, bit-identical to strtod. ToNumber validates and
accumulates in the same pass; everything else still goes through the copied
span and strtod. Whitespace trimming in Number(), parseInt, and parseFloat
skips the UTF-8 decode for ASCII non-whitespace.

toFixed: the exact-binary-value digit generation moves from scr_lib.c into
scr_number.c (scr_f64_to_fixed). For f <= 22, mantissa * 5^f fits a
64x64->128 multiply, so the spec's n is one shift plus the dropped-half bit
whenever it fits 64 bits. Digits render two at a time instead of through
snprintf. The 16-limb bignum remains the fallback.

Number to string (H44): a non-integral x below 2^46 that is the double
nearest to m/100 prints as m/100 directly instead of running Ryu. Below 2^46
ulp(x) < 0.01, so m/100 is the unique shortest round-tripping decimal. The
correctly rounded division (double)m / 100 == x is the proof. This covers
JSON.stringify, templates, String(), join, and console output.

Tests: Node oracles for every path. number-cases.txt gains money-like values,
the 2^46 cutoff, and one-ulp neighbors. tonumber-cases.txt gains fast-path
boundaries plus 20k seeded short decimals. The new parsefloat-cases.txt pins
scr_parse_float, and the new fixed-cases.txt/test_fixed.c pin toFixed,
including the 2^64 fast-path limit and f up to 100. Fuzz modes cover all four
paths. Existing case lines are unchanged. Corpus 3170 pins the paths
end-to-end against Node.

Linux x64 sandbox, A/B against perf/stack/06-h23-layout-noise,
--layouts=4 --runs=16 (run 3 of 3; geomean -6.0% / -6.7% / -6.3%):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 236.2 | 204.2 | -13.5% | -19.8% .. +2.1% | neutral | +0.0% | 488.62 | 0.42x |
| regex-logs | 176.2 | 168.8 | -4.2% | -10.1% .. +0.3% | neutral | +0.0% | 281.23 | 0.60x |
| ast-interp | 280.0 | 273.1 | -2.5% | -5.6% .. +1.4% | neutral | +2.8% | 273.05 | 1.00x |
| records-sort | 952.4 | 939.4 | -1.4% | -5.0% .. +5.6% | neutral | +0.0% | 289.87 | 3.24x |
| template-render | 78.9 | 77.3 | -2.1% | -6.2% .. +1.2% | neutral | +0.0% | 246.71 | 0.31x |
| functional-pipeline | 338.7 | 348.8 | +3.0% | -6.5% .. +24.0% | neutral | +0.1% | 339.84 | 1.03x |
| async-pipeline | 207.3 | 207.8 | +0.2% | -5.3% .. +3.8% | neutral | +0.0% | 105.57 | 1.97x |
| word-graph | 302.0 | 300.1 | -0.6% | -5.1% .. +12.9% | neutral | +3.0% | 600.48 | 0.50x |
| alloc-trees | 336.3 | 332.4 | -1.1% | -6.2% .. +2.6% | neutral | +0.1% | 144.73 | 2.30x |
| numeric-kernels | 49.7 | 49.8 | +0.1% | -3.4% .. +6.5% | neutral | +4.0% | 143.73 | 0.35x |
| validate-errors | 179.1 | 182.9 | +2.1% | -5.8% .. +9.1% | neutral | +0.0% | 727.78 | 0.25x |
| csv-numbers | 349.0 | 233.8 | -33.0% | -37.5% .. -26.5% | faster | +0.1% | 541.77 | 0.43x |
| log-summary | 171.6 | 135.7 | -20.9% | -23.3% .. -17.7% | faster | +1.8% | 313.2 | 0.43x |
| inventory-report | 411.9 | 384.7 | -6.6% | -18.1% .. +5.5% | neutral | +2.7% | 521.27 | 0.74x |

Size: +64..+136 bytes per executable. The +1.8..+4.0% rows are one 4 KiB
page crossing (two for inventory-report).
This commit is contained in:
Malte Ubl
2026-10-08 15:49:53 -05:00
committed by GitHub
parent f9f7880860
commit e7eef59ec1
16 changed files with 95378 additions and 188 deletions
+4 -149
View File
@@ -7199,104 +7199,11 @@ ScrStr *scr_num_to_fixed0(double x) {
return r;
}
/* The explicit-fraction-digits toFixed needs the exact binary value, not
* the shortest decimal that round-trips to it: (1.005).toFixed(2) is
* "1.00", for example. Represent abs(x) * 10^f as
*
* mantissa * 5^f * 2^(binary_exponent + f)
*
* in a tiny base-2^32 integer, then shift right with the spec's
* round-half-up rule. The largest value handled here is below 1e21 with
* f=100, so the rounded integer is below 1e121 (402 bits); sixteen limbs
* leave comfortable headroom without heap allocation. */
#define SCR_FIXED_LIMBS 16
typedef struct {
uint32_t limb[SCR_FIXED_LIMBS]; /* little-endian */
int len;
} ScrFixedInt;
static void scr_fixed_normalize(ScrFixedInt *v) {
while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--;
}
static void scr_fixed_mul5(ScrFixedInt *v) {
uint64_t carry = 0;
for (int i = 0; i < v->len; i++) {
uint64_t p = (uint64_t)v->limb[i] * 5 + carry;
v->limb[i] = (uint32_t)p;
carry = p >> 32;
}
if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
}
static bool scr_fixed_bit(const ScrFixedInt *v, int bit) {
int word = bit / 32;
return word < v->len && ((v->limb[word] >> (bit % 32)) & 1u) != 0;
}
static void scr_fixed_shr(ScrFixedInt *v, int bits) {
int words = bits / 32;
int rem = bits % 32;
if (words >= v->len) {
v->limb[0] = 0;
v->len = 1;
return;
}
int n = v->len - words;
for (int i = 0; i < n; i++) {
uint32_t lo = v->limb[i + words] >> rem;
uint32_t hi =
rem != 0 && i + words + 1 < v->len
? v->limb[i + words + 1] << (32 - rem)
: 0;
v->limb[i] = lo | hi;
}
v->len = n;
scr_fixed_normalize(v);
}
static void scr_fixed_shl(ScrFixedInt *v, int bits) {
uint32_t out[SCR_FIXED_LIMBS] = {0};
int words = bits / 32;
int rem = bits % 32;
for (int i = 0; i < v->len; i++) {
int at = i + words;
out[at] |= v->limb[i] << rem;
if (rem != 0) out[at + 1] |= v->limb[i] >> (32 - rem);
}
int n = v->len + words + (rem != 0 ? 1 : 0);
memcpy(v->limb, out, sizeof out);
v->len = n;
scr_fixed_normalize(v);
}
static void scr_fixed_inc(ScrFixedInt *v) {
uint64_t carry = 1;
for (int i = 0; i < v->len && carry != 0; i++) {
uint64_t s = (uint64_t)v->limb[i] + carry;
v->limb[i] = (uint32_t)s;
carry = s >> 32;
}
if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
}
/* Divide in place by 1e9; each quotient limb still fits uint32_t because
* the carried remainder is below the divisor. Returns the remainder. */
static uint32_t scr_fixed_div1e9(ScrFixedInt *v) {
uint64_t rem = 0;
for (int i = v->len - 1; i >= 0; i--) {
uint64_t cur = (rem << 32) | v->limb[i];
v->limb[i] = (uint32_t)(cur / 1000000000u);
rem = cur % 1000000000u;
}
scr_fixed_normalize(v);
return (uint32_t)rem;
}
/* Number.prototype.toFixed(fractionDigits), with the argument already
* number-typed by the frontend. ToIntegerOrInfinity validation precedes
* the receiver's non-finite arm, as ECMA-262 requires. Invalid precision
* raises V8's catchable RangeError text; otherwise the result is +1. */
* raises V8's catchable RangeError text; otherwise the result is +1. The
* exact-binary-value digits come from scr_f64_to_fixed (scr_number.c). */
ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
double fd = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
if (!(fd >= 0 && fd <= 100)) {
@@ -7305,62 +7212,10 @@ ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
return NULL;
}
int f = (int)fd;
if (!isfinite(x) || fabs(x) >= 1e21) return scr_f64_to_scrstr(x);
bool neg = x < 0; /* false for -0, exactly like the spec's sign arm */
double a = neg ? -x : x;
uint64_t bits;
memcpy(&bits, &a, sizeof bits);
uint64_t mantissa = bits & ((1ull << 52) - 1);
int ieee_exp = (int)((bits >> 52) & 0x7ffu);
int binary_exp;
if (ieee_exp == 0) {
binary_exp = -1074;
} else {
mantissa |= 1ull << 52;
binary_exp = ieee_exp - 1023 - 52;
}
ScrFixedInt n = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2};
scr_fixed_normalize(&n);
for (int i = 0; i < f; i++) scr_fixed_mul5(&n);
int shift = binary_exp + f;
if (shift >= 0) {
scr_fixed_shl(&n, shift);
} else {
int right = -shift;
bool round_up = scr_fixed_bit(&n, right - 1);
scr_fixed_shr(&n, right);
if (round_up) scr_fixed_inc(&n);
}
/* Render the exact rounded integer through base-1e9 chunks, then place
* the decimal point f digits from the right (padding through zero). */
uint32_t chunks[SCR_FIXED_LIMBS];
int chunk_count = 0;
do {
chunks[chunk_count++] = scr_fixed_div1e9(&n);
} while (!(n.len == 1 && n.limb[0] == 0));
char digits[128];
int dlen = snprintf(digits, sizeof digits, "%u", chunks[chunk_count - 1]);
for (int i = chunk_count - 2; i >= 0; i--) {
dlen += snprintf(digits + dlen, sizeof digits - (size_t)dlen,
"%09u", chunks[i]);
}
char out[128];
int o = 0;
if (neg) out[o++] = '-';
int padded = dlen > f + 1 ? dlen : f + 1;
int integer_digits = padded - f;
int leading_zeros = padded - dlen;
for (int i = 0; i < padded; i++) {
if (f != 0 && i == integer_digits) out[o++] = '.';
out[o++] = i < leading_zeros ? '0' : digits[i - leading_zeros];
}
return scr_str_new(out, (size_t)o);
size_t len = scr_f64_to_fixed(x, (int)fd, out);
return scr_str_new(out, len);
}
/* Increment a decimal digit string in place. Returns true on overflow —
+411 -6
View File
@@ -129,6 +129,14 @@ int scr_f64_digits(double x, char digits[18], int *n_out) {
return k;
}
/* Two-digit decimal spellings 00..99, for integer rendering. */
static const char pairs[] =
"00010203040506070809" "10111213141516171819"
"20212223242526272829" "30313233343536373839"
"40414243444546474849" "50515253545556575859"
"60616263646566676869" "70717273747576777879"
"80818283848586878889" "90919293949596979899";
size_t scr_f64_to_str(double x, char *buf) {
if (isnan(x)) return (size_t)(stpcpy(buf, "NaN") - buf);
if (x == 0) return (size_t)(stpcpy(buf, "0") - buf); /* covers -0 */
@@ -148,12 +156,6 @@ size_t scr_f64_to_str(double x, char *buf) {
if (x <= 9007199254740991.0) {
uint64_t integer = (uint64_t)x;
if ((double)integer == x) {
static const char pairs[] =
"00010203040506070809" "10111213141516171819"
"20212223242526272829" "30313233343536373839"
"40414243444546474849" "50515253545556575859"
"60616263646566676869" "70717273747576777879"
"80818283848586878889" "90919293949596979899";
char digits[16];
char *begin = digits + sizeof digits;
while (integer >= 100) {
@@ -176,6 +178,40 @@ size_t scr_f64_to_str(double x, char *buf) {
}
}
/* Money-like values: x is the double nearest to m/100 for an integer m.
* Then the decimal m/100 round-trips to x, and below 2^46 (ulp(x) <=
* 2^-7 < 0.01) it is also the unique shortest spelling: every decimal
* with fewer significant digits lies on the 0.1 grid, at least 0.01 away
* from m/100 when m % 10 != 0, so it cannot share x's rounding interval
* (width <= ulp(x)); two-digit candidates are 0.01 apart for the same
* reason. m/100 is never a rounding midpoint (a dyadic m/100 is k/4,
* exactly representable here), so tie rules do not matter. x >= 0.01
* keeps it in fixed notation. The division is the proof: it is
* correctly rounded, so equality means x IS the nearest double to m/100. */
if (x < 70368744177664.0 /* 2^46 */) {
uint64_t m = (uint64_t)(x * 100.0 + 0.5);
if (m != 0 && (double)m / 100.0 == x) {
uint64_t integer = m / 100;
unsigned cents = (unsigned)(m - integer * 100);
/* cents != 0: integral x took the exact-integer path above. */
char digits[16];
char *begin = digits + sizeof digits;
do {
uint64_t quotient = integer / 10;
*--begin = (char)('0' + (unsigned)(integer - quotient * 10));
integer = quotient;
} while (integer != 0);
size_t count = (size_t)(digits + sizeof digits - begin);
memcpy(out, begin, count);
out += count;
*out++ = '.';
*out++ = (char)('0' + cents / 10);
if (cents % 10 != 0) *out++ = (char)('0' + cents % 10);
*out = '\0';
return (size_t)(out - buf);
}
}
char digits[18];
int n;
int k = scr_f64_digits(x, digits, &n);
@@ -222,3 +258,372 @@ size_t scr_f64_to_str(double x, char *buf) {
*out = '\0';
return (size_t)(out - buf);
}
/* ── decimal string → double fast path ────────────────────────────────
* scr_decimal_scan validates the WHOLE span p[0..n) as a decimal literal
* [+-]? (digits [. digits*] | . digits) ([eE][+-]? digits)?
* (no Infinity, no whitespace) and returns -1 when it is not one, 1 with
* *out set when Clinger's fast path applies, and 0 when the span is valid
* but needs a correctly rounded strtod. Validation and accumulation share
* one pass. When the significant digits form an integer m <= 2^53 and the
* decimal exponent e satisfies |e| <= 22, m and 10^|e| are both exact
* doubles, so one IEEE multiply or divide rounds the exact value m × 10^e
* once: bit-identical to a correctly rounded strtod. Exponents a little
* above 22 fold the excess into m while it stays exact. Zero mantissas are
* ±0 whatever the exponent. Anything else answers 0: more than 19
* significant digits, m > 2^53, or a large exponent. Assumes binary64
* arithmetic without excess precision (FLT_EVAL_METHOD 0), as on every
* scriptc target. */
static const double scr_pow10_exact[23] = {
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
int scr_decimal_scan(const char *p, size_t n, double *out) {
size_t i = 0;
bool neg = false;
if (i < n && (p[i] == '+' || p[i] == '-')) {
neg = p[i] == '-';
i++;
}
uint64_t m = 0;
int nd = 0; /* significant digits in m (leading zeros excluded) */
int e10 = 0; /* decimal exponent: fraction shift + explicit exponent */
bool exact = true; /* false once a digit no longer fits m */
size_t digits = 0; /* integer + fraction digits, zeros included */
for (; i < n; i++) {
unsigned d = (unsigned)(unsigned char)p[i] - '0';
if (d > 9) break;
digits++;
if (m == 0 && d == 0) continue; /* leading integer zeros */
if (nd == 19) {
exact = false;
continue;
}
m = m * 10 + d;
nd++;
}
if (i < n && p[i] == '.') {
for (i++; i < n; i++) {
unsigned d = (unsigned)(unsigned char)p[i] - '0';
if (d > 9) break;
digits++;
if (m == 0 && d == 0) { /* leading fraction zeros */
e10--;
continue;
}
if (nd == 19) {
exact = false;
continue;
}
m = m * 10 + d;
nd++;
e10--;
}
}
if (digits == 0) return -1; /* ".", "+", "e5" */
if (i < n && (p[i] == 'e' || p[i] == 'E')) {
i++;
bool eneg = false;
if (i < n && (p[i] == '+' || p[i] == '-')) {
eneg = p[i] == '-';
i++;
}
size_t ed = i;
int ev = 0;
for (; i < n && p[i] >= '0' && p[i] <= '9'; i++) {
if (ev < 100000) ev = ev * 10 + (p[i] - '0');
}
if (i == ed) return -1; /* "1e", "1e+": the exponent needs digits */
e10 += eneg ? -ev : ev;
}
if (i != n) return -1; /* trailing garbage ("1_000", "1.2.3", "12px") */
if (!exact) return 0;
if (m == 0) {
*out = neg ? -0.0 : 0.0;
return 1;
}
if (m > (UINT64_C(1) << 53)) return 0;
double v = (double)m; /* exact */
if (e10 < 0) {
if (e10 < -22) return 0;
v /= scr_pow10_exact[-e10];
} else if (e10 <= 22) {
v *= scr_pow10_exact[e10];
} else {
/* m × 10^(e10-22) may still be an exact integer below 2^53. */
if (e10 > 22 + 15) return 0;
for (int k = e10 - 22; k > 0; k--) {
if (m > (UINT64_C(1) << 53) / 10) return 0;
m *= 10;
}
v = (double)m * 1e22;
}
*out = neg ? -v : v;
return 1;
}
bool scr_decimal_fast(const char *p, size_t n, double *out) {
return scr_decimal_scan(p, n, out) > 0;
}
/* ── Number.prototype.toFixed digits ──────────────────────────────────
* toFixed(f) needs the exact binary value, not the shortest decimal that
* round-trips to it: (1.005).toFixed(2) is "1.00". With abs(x) =
* mantissa × 2^binary_exponent, the spec's n (the integer closest to
* abs(x) × 10^f, ties toward the larger n) is
*
* round_half_up(mantissa × 5^f × 2^(binary_exponent + f)).
*
* Fast path: f <= 22 keeps 5^f below 2^52, so mantissa × 5^f fits in 105
* bits (a 64×64→128 multiply); a right shift with the dropped-half bit as
* the round-up decision gives n exactly, and n < 2^64 renders without a
* bignum. Everything else (f > 22, n >= 2^64, left shifts past 64 bits)
* takes a tiny base-2^32 bignum: the largest value handled is below 1e21
* with f = 100, so n < 1e121 (402 bits) fits sixteen limbs. */
static inline uint64_t scr_mul_u64(uint64_t a, uint64_t b, uint64_t *hi) {
#if defined(HAS_UINT128)
uint128_t p = (uint128_t)a * b;
*hi = (uint64_t)(p >> 64);
return (uint64_t)p;
#else
return umul128(a, b, hi);
#endif
}
/* n for f <= 22 when it fits 64 bits; false otherwise. */
static bool scr_fixed_fast(uint64_t mantissa, int binary_exp, int f, uint64_t *n_out) {
static const uint64_t pow5[23] = {
UINT64_C(1),
UINT64_C(5),
UINT64_C(25),
UINT64_C(125),
UINT64_C(625),
UINT64_C(3125),
UINT64_C(15625),
UINT64_C(78125),
UINT64_C(390625),
UINT64_C(1953125),
UINT64_C(9765625),
UINT64_C(48828125),
UINT64_C(244140625),
UINT64_C(1220703125),
UINT64_C(6103515625),
UINT64_C(30517578125),
UINT64_C(152587890625),
UINT64_C(762939453125),
UINT64_C(3814697265625),
UINT64_C(19073486328125),
UINT64_C(95367431640625),
UINT64_C(476837158203125),
UINT64_C(2384185791015625),
};
if (f > 22) return false;
uint64_t hi;
uint64_t lo = scr_mul_u64(mantissa, pow5[f], &hi); /* < 2^105 */
int shift = binary_exp + f;
if (shift >= 0) {
if (hi != 0 || shift >= 64 || (shift > 0 && (lo >> (64 - shift)) != 0)) return false;
*n_out = lo << shift;
return true;
}
int right = -shift;
if (right > 106) { /* the product is < 2^105: value < 1/2, n = 0 */
*n_out = 0;
return true;
}
/* round = bit (right-1); q = (hi:lo) >> right. */
int rb = right - 1;
uint64_t round = rb >= 64 ? (hi >> (rb - 64)) & 1 : (lo >> rb) & 1;
uint64_t q;
if (right >= 64) {
q = right == 64 ? hi : hi >> (right - 64);
} else {
if ((hi >> right) != 0) return false; /* q needs more than 64 bits */
q = (lo >> right) | (hi << (64 - right));
}
if (round && q == UINT64_MAX) return false;
*n_out = q + round;
return true;
}
#define SCR_FIXED_LIMBS 16
typedef struct {
uint32_t limb[SCR_FIXED_LIMBS]; /* little-endian */
int len;
} ScrFixedInt;
static void scr_fixed_normalize(ScrFixedInt *v) {
while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--;
}
static void scr_fixed_mul5(ScrFixedInt *v) {
uint64_t carry = 0;
for (int i = 0; i < v->len; i++) {
uint64_t p = (uint64_t)v->limb[i] * 5 + carry;
v->limb[i] = (uint32_t)p;
carry = p >> 32;
}
if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
}
static bool scr_fixed_bit(const ScrFixedInt *v, int bit) {
int word = bit / 32;
return word < v->len && ((v->limb[word] >> (bit % 32)) & 1u) != 0;
}
static void scr_fixed_shr(ScrFixedInt *v, int bits) {
int words = bits / 32;
int rem = bits % 32;
if (words >= v->len) {
v->limb[0] = 0;
v->len = 1;
return;
}
int n = v->len - words;
for (int i = 0; i < n; i++) {
uint32_t lo = v->limb[i + words] >> rem;
uint32_t hi =
rem != 0 && i + words + 1 < v->len
? v->limb[i + words + 1] << (32 - rem)
: 0;
v->limb[i] = lo | hi;
}
v->len = n;
scr_fixed_normalize(v);
}
static void scr_fixed_shl(ScrFixedInt *v, int bits) {
uint32_t out[SCR_FIXED_LIMBS] = {0};
int words = bits / 32;
int rem = bits % 32;
for (int i = 0; i < v->len; i++) {
int at = i + words;
out[at] |= v->limb[i] << rem;
if (rem != 0) out[at + 1] |= v->limb[i] >> (32 - rem);
}
int n = v->len + words + (rem != 0 ? 1 : 0);
memcpy(v->limb, out, sizeof out);
v->len = n;
scr_fixed_normalize(v);
}
static void scr_fixed_inc(ScrFixedInt *v) {
uint64_t carry = 1;
for (int i = 0; i < v->len && carry != 0; i++) {
uint64_t s = (uint64_t)v->limb[i] + carry;
v->limb[i] = (uint32_t)s;
carry = s >> 32;
}
if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
}
/* Divide in place by 1e9; each quotient limb still fits uint32_t because
* the carried remainder is below the divisor. Returns the remainder. */
static uint32_t scr_fixed_div1e9(ScrFixedInt *v) {
uint64_t rem = 0;
for (int i = v->len - 1; i >= 0; i--) {
uint64_t cur = (rem << 32) | v->limb[i];
v->limb[i] = (uint32_t)(cur / 1000000000u);
rem = cur % 1000000000u;
}
scr_fixed_normalize(v);
return (uint32_t)rem;
}
/* Writes the decimal digits of n (no leading zeros; "0" for zero) ending
* just before `end`; returns the first digit. */
static char *scr_u64_digits_back(uint64_t n, char *end) {
char *p = end;
while (n >= 100) {
uint64_t q = n / 100;
p -= 2;
memcpy(p, pairs + (n - q * 100) * 2, 2);
n = q;
}
if (n >= 10) {
p -= 2;
memcpy(p, pairs + n * 2, 2);
} else {
*--p = (char)('0' + (unsigned)n);
}
return p;
}
size_t scr_f64_to_fixed(double x, int f, char *buf) {
bool neg = x < 0; /* false for -0, exactly like the spec's sign arm */
double a = neg ? -x : x;
uint64_t bits;
memcpy(&bits, &a, sizeof bits);
uint64_t mantissa = bits & ((UINT64_C(1) << 52) - 1);
int ieee_exp = (int)((bits >> 52) & 0x7ffu);
int binary_exp;
if (ieee_exp == 0) {
binary_exp = -1074;
} else {
mantissa |= UINT64_C(1) << 52;
binary_exp = ieee_exp - 1023 - 52;
}
/* digits[] holds the decimal expansion of n, right-aligned. */
char digits[128];
char *dend = digits + sizeof digits;
char *dbeg;
uint64_t n64;
if (scr_fixed_fast(mantissa, binary_exp, f, &n64)) {
dbeg = scr_u64_digits_back(n64, dend);
} else {
ScrFixedInt n = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2};
scr_fixed_normalize(&n);
for (int i = 0; i < f; i++) scr_fixed_mul5(&n);
int shift = binary_exp + f;
if (shift >= 0) {
scr_fixed_shl(&n, shift);
} else {
int right = -shift;
bool round_up = scr_fixed_bit(&n, right - 1);
scr_fixed_shr(&n, right);
if (round_up) scr_fixed_inc(&n);
}
/* Base-1e9 chunks, least significant first; all but the leading
* chunk render as exactly nine digits. */
dbeg = dend;
for (;;) {
uint32_t chunk = scr_fixed_div1e9(&n);
bool last = n.len == 1 && n.limb[0] == 0;
char *stop = dbeg - 9;
dbeg = scr_u64_digits_back(chunk, dbeg);
if (last) break;
while (dbeg > stop) *--dbeg = '0';
}
}
int dlen = (int)(dend - dbeg);
/* Place the decimal point f digits from the right, padding through
* zero ("0.05"), with a leading "-" for negative x (even when n is 0). */
char *o = buf;
if (neg) *o++ = '-';
int padded = dlen > f + 1 ? dlen : f + 1;
int integer_digits = padded - f;
int leading_zeros = padded - dlen;
if (leading_zeros >= integer_digits) {
/* 0.000ddd: the integer part is a single zero. */
*o++ = '0';
if (f != 0) {
*o++ = '.';
for (int i = integer_digits; i < leading_zeros; i++) *o++ = '0';
memcpy(o, dbeg, (size_t)dlen);
o += dlen;
}
} else {
/* leading_zeros == 0 here: dlen >= f + 1. */
memcpy(o, dbeg, (size_t)integer_digits);
o += integer_digits;
if (f != 0) {
*o++ = '.';
memcpy(o, dbeg + integer_digits, (size_t)f);
o += f;
}
}
*o = '\0';
return (size_t)(o - buf);
}
+18
View File
@@ -6036,6 +6036,24 @@ size_t scr_f64_to_str(double x, char *buf);
* Returns k, the digit count (≤ 17). */
int scr_f64_digits(double x, char digits[18], int *n_out);
/* Clinger's exact fast path for a VALIDATED decimal span ([+-]? digits
* with optional fraction and exponent; no whitespace, no Infinity): sets
* *out and returns true when the value is m × 10^e with m <= 2^53 and a
* small |e| (one correctly rounded multiply/divide, bit-identical to
* strtod); false means the caller must take its strtod path. */
bool scr_decimal_fast(const char *p, size_t n, double *out);
/* The same fast path with whole-span validation folded into its scan: -1
* when p[0..n) is not such a decimal literal, 1 with *out set when exact,
* 0 when valid but the caller must use strtod. */
int scr_decimal_scan(const char *p, size_t n, double *out);
/* Number.prototype.toFixed's digit string for finite |x| < 1e21 and
* 0 <= f <= 100: the spec's n (closest to |x| × 10^f on the EXACT binary
* value, ties up) with the point placed f digits from the right and "-"
* for x < 0. Writes NUL-terminated text into buf (>= 128 bytes); returns
* the length. */
size_t scr_f64_to_fixed(double x, int f, char *buf);
/* ToString for template literals / string coercion. Returns +1. */
ScrStr *scr_f64_to_scrstr(double x);
ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */
+20 -29
View File
@@ -1684,6 +1684,8 @@ double scr_parse_int(ScrStr *s, double radix) {
const char *p = s->data;
size_t n = s->len, i = 0;
while (i < n) {
unsigned char c = (unsigned char)p[i];
if (c > 0x20 && c < 0x80) break; /* ASCII non-whitespace */
size_t adv;
uint32_t cp = scr_utf8_decode(p + i, &adv);
if (!scr_is_js_whitespace(cp)) break;
@@ -1729,6 +1731,8 @@ double scr_parse_float(ScrStr *s) {
const char *p = s->data;
size_t n = s->len, i = 0;
while (i < n) {
unsigned char c = (unsigned char)p[i];
if (c > 0x20 && c < 0x80) break; /* ASCII non-whitespace */
size_t adv;
uint32_t cp = scr_utf8_decode(p + i, &adv);
if (!scr_is_js_whitespace(cp)) break;
@@ -1767,6 +1771,8 @@ double scr_parse_float(ScrStr *s) {
if (j > ed) end = j; /* exponent joins only with digits ("1e" is 1) */
}
size_t span = end - start;
double fast;
if (scr_decimal_fast(p + start, span, &fast)) return fast;
char buf[64];
char *tmp = span < sizeof(buf) ? buf : malloc(span + 1);
if (!tmp) scr_oom();
@@ -1793,12 +1799,16 @@ double scr_string_to_number(ScrStr *s) {
const char *p = s->data;
size_t b = 0, e = s->len;
while (b < e) {
unsigned char c = (unsigned char)p[b];
if (c > 0x20 && c < 0x80) break; /* ASCII non-whitespace */
size_t adv;
uint32_t cp = scr_utf8_decode(p + b, &adv);
if (!scr_is_js_whitespace(cp)) break;
b += adv;
}
while (e > b) {
unsigned char c = (unsigned char)p[e - 1];
if (c > 0x20 && c < 0x80) break; /* ASCII non-whitespace */
size_t cs = e - 1; /* back up to the lead byte of the last char */
while (cs > b && ((unsigned char)p[cs] & 0xC0) == 0x80) cs--;
size_t adv;
@@ -1824,37 +1834,18 @@ double scr_string_to_number(ScrStr *s) {
return scr_digits_to_double(p + dig_start, i - dig_start, radix);
}
/* StrDecimalLiteral, whole-span: [+-]? (Infinity | digits [. digits*]
* | . digits) ([eE][+-]?digits)? — nothing before, nothing after. */
size_t i = 0;
double sign = 1.0;
if (p[0] == '+' || p[0] == '-') {
if (p[0] == '-') sign = -1.0;
i = 1;
}
* | . digits) ([eE][+-]?digits)? — nothing before, nothing after. The
* digit grammar and Clinger's exact fast path share one scan
* (scr_decimal_scan, scr_number.c); valid spans it cannot convert exactly
* fall through to strtod. */
size_t i = (p[0] == '+' || p[0] == '-') ? 1 : 0;
if (n - i == 8 && memcmp(p + i, "Infinity", 8) == 0) {
return sign * (double)INFINITY;
return p[0] == '-' ? -(double)INFINITY : (double)INFINITY;
}
size_t int_digits = 0, frac_digits = 0;
while (i < n && p[i] >= '0' && p[i] <= '9') {
i++;
int_digits++;
}
if (i < n && p[i] == '.') {
i++;
while (i < n && p[i] >= '0' && p[i] <= '9') {
i++;
frac_digits++;
}
}
if (int_digits == 0 && frac_digits == 0) return NAN; /* ".", "+", "e5" */
if (i < n && (p[i] == 'e' || p[i] == 'E')) {
i++;
if (i < n && (p[i] == '+' || p[i] == '-')) i++;
size_t ed = i;
while (i < n && p[i] >= '0' && p[i] <= '9') i++;
if (i == ed) return NAN; /* "1e", "1e+" — exponent needs digits */
}
if (i != n) return NAN; /* trailing garbage ("1_000", "1.2.3", "12px") */
double fast;
int scanned = scr_decimal_scan(p, n, &fast);
if (scanned < 0) return NAN; /* ".", "1e", "1.2.3", "12px", "1_000" */
if (scanned > 0) return fast;
char buf[64];
char *tmp = n < sizeof(buf) ? buf : malloc(n + 1);
if (!tmp) scr_oom();
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
/* oxlint-disable no-loss-of-precision -- Oracle inputs intentionally exercise numeric literal rounding. */
// Generates Number.prototype.toFixed test cases with Node as the oracle.
// Each line: <16 hex digits of the double's bit pattern>\t<fractionDigits>\t<x.toFixed(f)>
// Only finite |x| < 1e21 (the scr_f64_to_fixed domain; larger values and
// non-finite receivers fall back to ToString before digit generation).
//
// node gen-fixed-cases.mjs cases # curated + seeded cases → stdout
// node gen-fixed-cases.mjs fuzz <count> # <count> random cases → stdout
import { argv, stdout } from "node:process";
const f64 = new Float64Array(1);
const u64 = new BigUint64Array(f64.buffer);
function line(x, f) {
f64[0] = x;
return `${u64[0].toString(16).padStart(16, "0")}\t${f}\t${x.toFixed(f)}\n`;
}
function* curated() {
const values = [
0,
-0,
1,
-1,
0.5,
-0.5,
1.5,
2.5,
-2.5,
0.05,
0.005,
0.0005,
1.005,
1.015,
1.025,
1.045,
2.675,
8.345,
0.285,
10.235,
0.1 + 0.2,
123.456,
99.995,
999.995,
0.000001,
1e-7,
5e-324,
-5e-324,
2.2250738585072014e-308,
Number.EPSILON,
2 ** 52,
2 ** 52 + 0.5,
2 ** 53,
2 ** 53 + 2,
2 ** 63,
2 ** 64,
2 ** 64 - 2048,
1.8446744073709552e19,
1e17,
1e18,
1e19,
1e20,
999999999999999900000,
123456789.987654321,
Math.PI,
Math.E,
-Math.PI,
1 / 3,
2 / 3,
];
const digits = [0, 1, 2, 3, 5, 10, 15, 17, 20, 21, 22, 23, 25, 30, 50, 99, 100];
for (const x of values) for (const f of digits) yield [x, f];
// Where the 64-bit fast path stops: n = |x| × 10^f just below / above 2^64.
for (let f = 0; f <= 23; f++) {
for (const scale of [0.5, 0.9, 1, 1.1, 2]) {
const x = (2 ** 64 / 10 ** f) * scale;
if (x < 1e21) {
yield [x, f];
yield [-x, f];
}
}
}
// Money-like values and exact halves at small digit counts.
for (let m = 0; m <= 2000; m++) {
yield [m / 100, 2];
yield [m / 1000, 2];
if (m % 7 === 0) yield [-m / 100, 1];
if (m % 5 === 0) yield [m / 8, 2];
}
}
// xorshift128 — deterministic so the committed file is reproducible
function makeRng(seed) {
let s0 = seed ^ 0x9e3779b97f4a7c15n,
s1 = 0x2545f4914f6cdd1dn;
return () => {
let x = s0;
const y = s1;
s0 = y;
x ^= (x << 23n) & 0xffffffffffffffffn;
s1 = x ^ y ^ (x >> 17n) ^ (y >> 26n);
return (s1 + y) & 0xffffffffffffffffn;
};
}
function* random(count, seed) {
const next = makeRng(seed);
const int = (n) => Number(next() % BigInt(n));
let emitted = 0;
while (emitted < count) {
const shape = int(4);
let x;
let f;
if (shape === 0) {
// Raw random bit patterns (mostly tiny or huge) at any precision.
u64[0] = next();
x = f64[0];
f = int(101);
} else if (shape === 1) {
// Random magnitudes across the fast-path range.
x = (int(2 ** 30) / 2 ** 30) * 10 ** (int(30) - 8);
f = int(26);
} else if (shape === 2) {
// Money-like m / 100 and m / 1000 with typical digit counts.
x = int(10 ** (1 + int(14))) / (int(2) ? 100 : 1000);
f = int(5);
} else {
// Arithmetic results (revenue-style products) with small f.
x = (int(100000) / 100) * int(50) * (1 - int(300) / 1000);
f = int(4);
}
if (int(3) === 0) x = -x;
if (!Number.isFinite(x) || Math.abs(x) >= 1e21) continue;
yield [x, f];
emitted++;
}
}
const mode = argv[2] ?? "cases";
let chunk = "";
function emit([x, f]) {
chunk += line(x, f);
if (chunk.length > 1 << 16) {
stdout.write(chunk);
chunk = "";
}
}
if (mode === "cases") {
for (const c of curated()) emit(c);
for (const c of random(30_000, 0xf1c3dn)) emit(c);
} else if (mode === "fuzz") {
const count = Number(argv[3] ?? 1_000_000);
const seed = BigInt(Date.now());
for (const c of random(count, seed)) emit(c);
} else {
throw new Error(`unknown mode: ${mode}`);
}
stdout.write(chunk);
+57 -1
View File
@@ -76,6 +76,40 @@ function* curated() {
const x = 10 ** d;
yield* [x - 1, x, x + 1];
}
// Money-like values (the two-decimal fast path): m / 100 across every
// magnitude, around the 2^46 cutoff, their neighbors one ulp away, and
// spellings whose x * 100 is close to but not exactly integral.
yield* [0.01, 0.05, 0.1, 0.5, 0.99, 1.01, 1.1, 12.34, 99.99, 100.01, 1.005, 1.015, 0.285];
yield* [0.1 + 0.2, 0.7 + 0.1, 1.1 * 1.1, 4.35 * 100, 0.07 * 3, 19.99 * 3, 1 / 100, 3 / 100];
const cutoff = 2 ** 46;
for (const base of [cutoff, cutoff / 2, cutoff * 2, 2 ** 43, 2 ** 53 / 100]) {
for (let k = -3; k <= 3; k++) {
for (const c of [0.01, 0.1, 0.25, 0.5, 0.75, 0.99]) {
const x = Math.floor(base) + k + c;
yield* [x, -x];
}
}
}
const next = makeRng(0x6d6f6e6579n);
const int = (n) => Number(next() % BigInt(n));
for (let i = 0; i < 4000; i++) {
const digits = 1 + int(17);
const m = int(10 ** Math.min(digits, 15)) * 10 ** Math.max(0, digits - 15) + int(100);
const x = m / 100;
yield x;
if (i % 4 === 0) yield -x;
if (i % 8 === 0) {
// one ulp either side: no longer the double nearest to m / 100
f64[0] = x;
u64[0] += 1n;
yield f64[0];
f64[0] = x;
if (u64[0] > 0n) u64[0] -= 1n;
yield f64[0];
}
if (i % 16 === 0) yield m / 10;
if (i % 16 === 8) yield m / 1000;
}
}
// xorshift128 — deterministic so the committed file is reproducible
@@ -102,6 +136,27 @@ function* random(count, seed) {
}
}
// Money-like doubles for the two-decimal fast path: m / 100 (and m / 10,
// m / 1000) at every magnitude up to 1e17, products of such values, and
// neighbors one ulp away. Fuzz-only, so the committed file is unchanged.
function* moneyLike(count, seed) {
const next = makeRng(seed);
const int = (n) => Number(next() % BigInt(n));
for (let emitted = 0; emitted < count; emitted++) {
const m = int(10 ** (1 + int(15))) * 10 ** int(3) + int(100);
let x = m / [100, 100, 100, 10, 1000][int(5)];
const shape = int(8);
if (shape === 0) x *= int(50);
else if (shape === 1) x = (x * (1000 - int(300))) / 1000;
else if (shape === 2) {
f64[0] = x;
u64[0] += int(2) ? 1n : -1n;
x = f64[0];
}
yield int(4) === 0 ? -x : x;
}
}
const mode = argv[2] ?? "cases";
let chunk = "";
function emit(x) {
@@ -118,7 +173,8 @@ if (mode === "cases") {
} else if (mode === "fuzz") {
const count = Number(argv[3] ?? 1_000_000);
const seed = BigInt(Date.now());
for (const x of random(count, seed)) emit(x);
for (const x of random(count >> 1, seed)) emit(x);
for (const x of moneyLike(count - (count >> 1), seed + 1n)) emit(x);
} else {
throw new Error(`unknown mode: ${mode}`);
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+23
View File
@@ -29,3 +29,26 @@ test("scr_f64_to_str matches Node String(x) on committed oracle cases", async ()
const { stderr } = await execFileAsync(bin, [join(testDir, "number-cases.txt")]);
expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
});
// toFixed's exact-binary-value digits (scr_f64_to_fixed: the 64-bit fast
// path and the bignum fallback) against Node's x.toFixed(f), from the
// committed case file generated by gen-fixed-cases.mjs.
test("scr_f64_to_fixed matches Node toFixed on committed oracle cases", async () => {
const buildDir = join(testDir, "build");
await mkdir(buildDir, { recursive: true });
const bin = join(buildDir, "test_fixed");
await execFileAsync("clang", [
"-std=c11",
"-O2",
"-Wall",
"-Wextra",
...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
"-o",
bin,
join(testDir, "test_fixed.c"),
join(testDir, "../src/scr_number.c"),
...(process.platform === "linux" ? ["-lm"] : []),
]);
const { stderr } = await execFileAsync(bin, [join(testDir, "fixed-cases.txt")]);
expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
});
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
/* Oracle test for scr_f64_to_fixed (Number.prototype.toFixed's digits).
* Reads case lines ("<16-hex-digit bit pattern>\t<fractionDigits>\t
* <expected>\n" — see gen-fixed-cases.mjs) from the file given as argv[1]
* (or stdin), renders each double, and asserts byte equality with Node's
* x.toFixed(f).
* Exit 0 = all pass; prints each mismatch (capped) and exits 1 otherwise.
*/
#include "../src/scr_runtime.h"
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
FILE *in = stdin;
if (argc > 1) {
in = fopen(argv[1], "r");
if (!in) {
perror(argv[1]);
return 2;
}
}
char linebuf[256];
char got[128];
long total = 0, failed = 0;
while (fgets(linebuf, sizeof linebuf, in)) {
char *tab = strchr(linebuf, '\t');
char *tab2 = tab ? strchr(tab + 1, '\t') : NULL;
if (!tab2) {
failed++;
fprintf(stderr, "BAD LINE: %s\n", linebuf);
continue;
}
*tab = '\0';
*tab2 = '\0';
int f = atoi(tab + 1);
char *expected = tab2 + 1;
expected[strcspn(expected, "\n")] = '\0';
union {
uint64_t u;
double d;
} bits;
bits.u = strtoull(linebuf, NULL, 16);
size_t len = scr_f64_to_fixed(bits.d, f, got);
total++;
if (len != strlen(got) || strcmp(got, expected) != 0) {
failed++;
if (failed <= 20) {
fprintf(stderr, "MISMATCH bits=%s f=%d expected=\"%s\" got=\"%s\"\n",
linebuf, f, expected, got);
}
}
}
if (in != stdin) fclose(in);
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
return failed ? 1 : 0;
}
+4 -2
View File
@@ -1,4 +1,5 @@
/* Oracle test for scr_string_to_number (ToNumber over strings).
/* Oracle test for scr_string_to_number (ToNumber over strings), or for
* scr_parse_float when argv[2] is "parseFloat" (parsefloat-cases.txt).
* Reads case lines ("<input-hex or ->\t<16-hex-digit expected bit
* pattern>\n" — see gen-tonumber-cases.mjs) from the file given as
* argv[1] (or stdin), parses each input, and asserts the resulting
@@ -49,6 +50,7 @@ int main(int argc, char **argv) {
}
}
bool parse_float = argc > 2 && strcmp(argv[2], "parseFloat") == 0;
static char linebuf[2 * MAX_FIELD + 32];
static char input_bytes[MAX_FIELD];
long total = 0, failed = 0;
@@ -75,7 +77,7 @@ int main(int argc, char **argv) {
expected.u = strtoull(expected_hex, NULL, 16);
ScrStr *s = scr_str_new(input_bytes, in_len);
got.d = scr_string_to_number(s);
got.d = parse_float ? scr_parse_float(s) : scr_string_to_number(s);
scr_str_release(s);
total++;
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -17,7 +17,7 @@ const testDir = import.meta.dirname;
// with giant exact-integer inputs, and the garbage shapes that must be
// NaN. Built with ASan + the RC audit so the ~50k allocate/parse/release
// round-trips also prove the parser neither leaks nor double-frees.
test("scr_string_to_number matches Node Number(s) on committed oracle cases", async () => {
test("scr_string_to_number and scr_parse_float match Node on committed oracle cases", async () => {
const buildDir = join(testDir, "build");
await mkdir(buildDir, { recursive: true });
const bin = join(buildDir, "test_tonumber");
@@ -59,4 +59,8 @@ test("scr_string_to_number matches Node Number(s) on committed oracle cases", as
]);
const { stderr } = await execFileAsync(bin, [join(testDir, "tonumber-cases.txt")]);
expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
// The same binary checks scr_parse_float against Node's parseFloat over
// the same inputs (gen-tonumber-cases.mjs cases --parse-float).
const parsed = await execFileAsync(bin, [join(testDir, "parsefloat-cases.txt"), "parseFloat"]);
expect(parsed.stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
}, 120_000);
@@ -0,0 +1,71 @@
// Number <-> string fast paths, pinned against Node both ways: short
// decimal spellings through Number(), unary plus, and parseFloat (the
// exact Clinger path and its strtod fallback at the 2^53 / 10^22 edges),
// toFixed on money values, exact halves, and the 2^64 fast-path limit, and
// shortest String()/template/JSON output for two-decimal values on both
// sides of the 2^46 cutoff.
const spellings: string[] = [
"0", "-0", "+0", "00", "0.0", "-0.00", "0e999", "-0e-999", ".5", "5.", "-.25",
"1", "42", "-17", "199", "12.34", "-12.34", "0.07", "0.1", "0.3", "123.4500", "007.25",
"1e22", "1e23", "1e-22", "1e-23", "9e37", "123e30", "1.5e-7", "2.5E+3",
"9007199254740991", "9007199254740992", "9007199254740993", "9007199254740993e2",
"4503599627370497e22", "1234567890123456789", "12345678901234567890",
"0.1234567890123456789", "1.000000000000000000001", " 12.5 ", "\t-3.75\n",
"12px", "1e", "1.2.3", "Infinity", "-Infinity", "0x1f", "0b101", "-0x10",
];
for (let i = 0; i < spellings.length; i++) {
const s = spellings[i]!;
const n = Number(s);
const u = +s;
const f = parseFloat(s);
// 1/x distinguishes -0 from +0.
console.log(JSON.stringify(s), n, 1 / n, Object.is(u, n), f, 1 / f);
}
// toFixed: money values, exact halves (ties up on the exact binary value),
// the 2^64 boundary of the 64-bit fast path, and the bignum fallback.
const fixedInputs: number[] = [
0, -0, 0.5, 1.5, 2.5, -2.5, 0.005, 1.005, 1.015, 2.675, 8.345, 10.235, 99.995,
0.1 + 0.2, 123.456, 5e-324, 1e-7, 2 ** 53, 2 ** 64, 2 ** 64 - 2048, 1e19, 1e20,
184467440737.09552, 18446744073.709552, -18446744073709.552, Math.PI, 1 / 3,
];
for (const x of fixedInputs) {
console.log(x.toFixed(0), x.toFixed(2), x.toFixed(3), x.toFixed(10), x.toFixed(22), x.toFixed(23));
}
console.log((1.005).toFixed(50));
console.log((5e-324).toFixed(100));
// Shortest output for money-like doubles: m / 100 spellings, values one
// ulp away, arithmetic results that are not exactly m / 100, and the 2^46
// cutoff where the two-decimal path hands over to Ryu.
const cutoff = 2 ** 46;
const shortest: number[] = [
0.01, 0.05, 0.1, 0.5, 0.99, 1.01, 12.34, 99.99, 100.01, 1.005, 0.285,
0.1 + 0.2, 0.7 + 0.1, 1.1 * 1.1, 4.35 * 100, 19.99 * 3, 1 / 100,
cutoff - 1.25, cutoff - 0.75, cutoff - 0.5, cutoff + 0.25, cutoff + 0.5, cutoff * 2 + 0.5,
70368744177663.99, 70368744177663.01, 9007199254740.99, 900719925474.09,
];
let total = 0;
for (let m = 1; m < 400; m += 7) {
const x = m / 100;
shortest.push(x, -x, x + 1e9, (x * 3) / 7);
total += x;
}
shortest.push(total);
for (const x of shortest) {
console.log(String(x), `${-x}`, JSON.stringify({ x }), [x, x * 10].join("|"));
}
// The same conversions in the CSV-like shape the benchmarks use.
let revenue = 0;
const rows = ["3,19.99,0.15", "10,0.07,0", "1,1234.5,0.3", "7,2.675,0.125"];
for (const row of rows) {
const cols = row.split(",");
const count = Number(cols[0]);
const price = Number(cols[1]);
const discount = Number(cols[2]);
const line = count * price * (1 - discount);
revenue += line;
console.log(line.toFixed(2) + "|" + String(count), `${price}`, JSON.stringify([price, discount]));
}
console.log(revenue.toFixed(2), String(Math.round(revenue * 100) / 100));