Files
scriptc/packages/runtime/test/gen-tonumber-cases.mjs
Chris Tate 5d6abeaefe JS-exact StringToNumber in the runtime, fuzz-verified against Node
- scr_string_to_number: ECMA-262 StringNumericLiteral exactly — StrWhiteSpace trim (empty → +0), signed decimals through strtod over the validated span, unsigned 0x/0o/0b through the exact bignum rounding, trailing garbage → NaN
- oracle suite mirrors the formatting precedent: gen-tonumber-cases.mjs (Node is the oracle), 50k committed cases asserting bit-equal doubles under ASan + RC audit
- fuzzed once against Node on 1,000,000 mixed-shape strings: 1000000/1000000 bit-exact
2026-07-22 20:02:10 -05:00

232 lines
8.9 KiB
JavaScript

// Generates ToNumber(string) test cases with Node as the oracle — the
// scr_string_to_number counterpart of gen-number-cases.mjs (formatting).
// Each line: <UTF-8 bytes of the input string as hex, "-" when empty>\t
// <16 hex digits of Number(input)'s bit pattern>
//
// node gen-tonumber-cases.mjs cases # curated + 50k seeded cases → stdout
// node gen-tonumber-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(s) {
f64[0] = Number(s);
const hex = Buffer.from(s, "utf8").toString("hex");
return `${hex === "" ? "-" : hex}\t${u64[0].toString(16).padStart(16, "0")}\n`;
}
// The complete StrWhiteSpace set: WhiteSpace (TAB VT FF SP NBSP ZWNBSP +
// the Zs category) and LineTerminator (LF CR LS PS).
const WS = [
"\t", "\n", "\v", "\f", "\r", " ", "\u00a0", "\u1680",
"\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006",
"\u2007", "\u2008", "\u2009", "\u200a", "\u2028", "\u2029", "\u202f",
"\u205f", "\u3000", "\ufeff",
];
function* curated() {
// Whitespace: empty, each StrWhiteSpaceChar alone, all of them joined,
// and NON-whitespace lookalikes (ZWSP, ZWNJ, MVS) that must be NaN.
yield "";
yield* WS;
yield WS.join("");
yield* ["\u200b", "\u200c", "\u200d", "\u180e", "\u0085"];
// Core decimals, signed and whitespace-decorated.
const core = [
"0", "-0", "+0", "1", "42", "-1", "0.5", ".5", "5.", "1.", "+.5", "-.5",
"0.1", "0.2", "0.3", "123.456", "3.141592653589793", "1e3", "1E3",
"1e+3", "1e-3", "5.e3", ".5e-3", "2e308", "1e309", "-1e309", "1e-309",
"1e-324", "5e-324", "2.5e-324", "2e-324", "7.5e-324", "1e-400", "1e400",
"9007199254740991", "9007199254740992", "9007199254740993",
"9007199254740993.0", "9007199254740994", "9007199254740995",
"1.7976931348623157e308", "1.7976931348623158e308",
"1.7976931348623159e308", "-1.7976931348623157e308",
"2.2250738585072014e-308", "2.2250738585072011e-308",
"4.9406564584124654e-324", "2.4703282292062327e-324",
"2.4703282292062328e-324", "1.0000000000000002", "1.0000000000000001",
"0.000001", "0.0000001", "1e21", "1e-7", "123456789012345678901",
"Infinity", "+Infinity", "-Infinity",
];
yield* core;
for (const s of core) {
yield ` ${s} `;
yield `\t\n${s}\u00a0\ufeff`;
yield `\u3000${s}\u2028`;
}
// Non-decimal integer literals: every prefix case, digit sweeps, the
// 2^53 neighborhood (rounding), giant exact-integer inputs (bignum
// rounding, half-even ties), and overflow past 2^1024 → Infinity.
const radixes = [
["0x", "0X"], ["0o", "0O"], ["0b", "0B"],
];
const digits = {
"0x": "0123456789abcdefABCDEF", "0o": "01234567", "0b": "01",
};
for (const [lo, up] of radixes) {
const d = digits[lo];
yield* [`${lo}0`, `${up}0`, `${lo}1`, `${up}1`, `${lo}${d}`, `${lo}00007`];
yield ` ${lo}11 `;
yield* [lo, up, `${lo} 1`, `-${lo}1`, `+${lo}1`, `${lo}1.5`, `${lo}1e2x`];
}
yield* [
"0x1fffffffffffff", "0x20000000000000", "0x20000000000001",
"0x20000000000002", "0x20000000000003", "0x20000000000004",
"0x3fffffffffffff", "0x40000000000001", "0x40000000000002",
"0x7fffffffffffffff", "0x8000000000000000", "0xffffffffffffffff",
"0x10000000000000000", "0x100000000000000001",
"0xfffffffffffff800000000000000000000", // half-even tie territory
"0xfffffffffffffbffffffffffffffffffff",
"0xfffffffffffffc00000000000000000000",
"0x" + "f".repeat(100), "0x" + "1" + "0".repeat(255), // < 2^1024
"0x" + "1" + "0".repeat(256), // = 2^1024 → Infinity
"0x" + "f".repeat(300), // way past → Infinity
"0b" + "1".repeat(53), "0b" + "1".repeat(54), "0b" + "1".repeat(55),
"0b1" + "0".repeat(52) + "1", "0b1" + "0".repeat(53) + "1",
"0b" + "1".repeat(1024), "0b1" + "0".repeat(1023),
"0b1" + "0".repeat(1024), "0o" + "7".repeat(400),
"0o1" + "0".repeat(340), "0x0000000000000000000000000000",
];
// Long decimal digit strings: exactness and overflow through strtod.
for (const k of [17, 20, 25, 50, 100, 308, 309, 310, 400, 800]) {
yield "9".repeat(k);
yield "1" + "0".repeat(k);
yield "0." + "0".repeat(k) + "1";
yield "0." + "1".repeat(k);
yield "1." + "9".repeat(k) + "e-100";
}
// Garbage shapes: every one must be NaN (asserted against Node either
// way — the oracle decides).
yield* [
"12px", "px12", "1.2.3", "1..2", "..5", "1_000", "1,000", "+-1", "-+1",
"--1", "++1", "1 2", "+ 1", "- 1", "NaN", "nan", "-NaN", "infinity",
"INFINITY", "Infinit", "Infinityy", "+Infinity8", "- Infinity",
"e5", ".e5", "+.", "-.", ".", "+", "-", "0x", "0xg", "0o8", "0b2",
"1e", "1e+", "1e-", "1e1.5", "1e+1e", "0x1_0", "0b1_0", "\u0661\u0662\u0663", "12\u0663",
"1\u200b", "\u200b1", "1", "1", "0x10 20", "true", "false",
"null", "undefined", "0 x10", "0 b10", "00x10", "00b1", "0.x1",
];
// Interior-whitespace garbage per whitespace char (trim is ends-only).
for (const w of WS) yield `1${w}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));
const pick = (arr) => arr[int(arr.length)];
const randDouble = () => {
u64[0] = next();
return f64[0];
};
const GARBAGE = "0123456789abcdefoxABX+-._e E\u00a0\ufeffpz$";
const digitsOf = { 16: "0123456789abcdefABCDEF", 8: "01234567", 2: "01", 10: "0123456789" };
let emitted = 0;
while (emitted < count) {
const shape = int(10);
let s;
if (shape === 0) {
// Round-trip: Node's own shortest formatting of a random double.
const x = randDouble();
s = String(x);
} else if (shape === 1) {
// Fixed/exponential renderings of a random double.
const x = randDouble();
if (!Number.isFinite(x)) continue;
s = int(2) ? x.toFixed(int(21)) : x.toExponential(int(21));
} else if (shape === 2 || shape === 3) {
// Random digit strings in a random radix with its prefix.
const radix = pick([16, 8, 2]);
const d = digitsOf[radix];
let body = "";
const len = 1 + int(shape === 2 ? 20 : 300);
for (let i = 0; i < len; i++) body += d[int(d.length)];
s = pick([radix === 16 ? "0x" : radix === 8 ? "0o" : "0b",
radix === 16 ? "0X" : radix === 8 ? "0O" : "0B"]) + body;
} else if (shape === 4) {
// Random decimal spellings: sign, int/frac digits, exponent.
const digs = (n) => {
let r = "";
for (let i = 0; i < n; i++) r += digitsOf[10][int(10)];
return r;
};
s = pick(["", "+", "-"]) + digs(int(25)) +
(int(2) ? "." + digs(int(25)) : "") +
(int(2) ? pick(["e", "E"]) + pick(["", "+", "-"]) + digs(int(4)) : "");
} else if (shape === 5) {
// Whitespace decoration around a valid spelling.
const deco = () => {
let r = "";
const n = int(4);
for (let i = 0; i < n; i++) r += pick(WS);
return r;
};
s = deco() + String(randDouble()) + deco();
} else if (shape === 6) {
// Mutation: a random char inserted into a valid spelling.
const base = String(randDouble());
const at = int(base.length + 1);
s = base.slice(0, at) + GARBAGE[int(GARBAGE.length)] + base.slice(at);
} else if (shape === 7) {
// Fully random short ASCII.
let r = "";
const len = int(12);
for (let i = 0; i < len; i++) r += String.fromCharCode(0x20 + int(0x5f));
s = r;
} else if (shape === 8) {
// Random characters from the numeric-adjacent alphabet.
let r = "";
const len = 1 + int(16);
for (let i = 0; i < len; i++) r += GARBAGE[int(GARBAGE.length)];
s = r;
} else {
// Random BMP code points (exercises the UTF-8 walk; surrogates
// skipped — the runtime stores well-formed UTF-8 only).
let r = "";
const len = int(8);
for (let i = 0; i < len; i++) {
const cp = int(0xffff);
if (cp >= 0xd800 && cp <= 0xdfff) continue;
r += String.fromCharCode(cp);
}
s = r;
}
yield s;
emitted++;
}
}
const mode = argv[2] ?? "cases";
let chunk = "";
function emit(s) {
chunk += line(s);
if (chunk.length > 1 << 16) {
stdout.write(chunk);
chunk = "";
}
}
if (mode === "cases") {
for (const s of curated()) emit(s);
for (const s of random(50_000, 0x70b0b0e5n)) emit(s);
} else if (mode === "fuzz") {
const count = Number(argv[3] ?? 1_000_000);
const seed = BigInt(Date.now());
for (const s of random(count, seed)) emit(s);
} else {
throw new Error(`unknown mode: ${mode}`);
}
stdout.write(chunk);