mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +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).
107 lines
3.1 KiB
C
107 lines
3.1 KiB
C
/* 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
|
|
* double is BIT-equal to Node's Number(input) (NaN payload and the sign
|
|
* of zero included).
|
|
* 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>
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
long scr_str_live_count(void); /* provided by scr_string.c */
|
|
#endif
|
|
|
|
#define MAX_FIELD 8192
|
|
|
|
static int hex_val(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
/* Decode "<hex>" or "-" (empty) into out; returns length or (size_t)-1. */
|
|
static size_t hex_decode(const char *hex, size_t hexlen, char *out) {
|
|
if (hexlen == 1 && hex[0] == '-') return 0;
|
|
if (hexlen % 2 != 0 || hexlen / 2 > MAX_FIELD) return (size_t)-1;
|
|
for (size_t i = 0; i < hexlen; i += 2) {
|
|
int hi = hex_val(hex[i]), lo = hex_val(hex[i + 1]);
|
|
if (hi < 0 || lo < 0) return (size_t)-1;
|
|
out[i / 2] = (char)((hi << 4) | lo);
|
|
}
|
|
return hexlen / 2;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
FILE *in = stdin;
|
|
if (argc > 1) {
|
|
in = fopen(argv[1], "r");
|
|
if (!in) {
|
|
perror(argv[1]);
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
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;
|
|
while (fgets(linebuf, sizeof linebuf, in)) {
|
|
char *tab = strchr(linebuf, '\t');
|
|
if (!tab) {
|
|
failed++;
|
|
fprintf(stderr, "BAD LINE (no tab)\n");
|
|
continue;
|
|
}
|
|
size_t in_len = hex_decode(linebuf, (size_t)(tab - linebuf), input_bytes);
|
|
char *expected_hex = tab + 1;
|
|
expected_hex[strcspn(expected_hex, "\n")] = '\0';
|
|
if (in_len == (size_t)-1 || strlen(expected_hex) != 16) {
|
|
failed++;
|
|
fprintf(stderr, "BAD LINE: %s\n", linebuf);
|
|
continue;
|
|
}
|
|
|
|
union {
|
|
uint64_t u;
|
|
double d;
|
|
} expected, got;
|
|
expected.u = strtoull(expected_hex, NULL, 16);
|
|
|
|
ScrStr *s = scr_str_new(input_bytes, in_len);
|
|
got.d = parse_float ? scr_parse_float(s) : scr_string_to_number(s);
|
|
scr_str_release(s);
|
|
|
|
total++;
|
|
if (got.u != expected.u) {
|
|
failed++;
|
|
if (failed <= 20) {
|
|
*tab = '\0';
|
|
fprintf(stderr,
|
|
"MISMATCH input=%s expected=%016" PRIx64 " got=%016" PRIx64
|
|
" (%.17g)\n",
|
|
linebuf, expected.u, got.u, got.d);
|
|
}
|
|
}
|
|
}
|
|
if (in != stdin) fclose(in);
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
if (scr_str_live_count() != 0) {
|
|
fprintf(stderr, "RC AUDIT: %ld strings leaked\n", scr_str_live_count());
|
|
failed++;
|
|
}
|
|
#endif
|
|
|
|
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
|
|
return failed ? 1 : 0;
|
|
}
|