mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
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:
@@ -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 —
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" */
|
||||
|
||||
@@ -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
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
Reference in New Issue
Block a user