mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
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).
160 lines
4.0 KiB
JavaScript
160 lines
4.0 KiB
JavaScript
/* 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);
|