mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
Merge main
This commit is contained in:
@@ -266,8 +266,10 @@ SCR_TL size_t scr_cyc_live = 0;
|
||||
SCR_TL size_t scr_cyc_old_freed = 0;
|
||||
#endif
|
||||
|
||||
/* Descriptor copies by registry id. Id 0 stays empty (an unregistered type). */
|
||||
ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES];
|
||||
/* Descriptor copies by registry id. Id 0 stays empty (an unregistered type).
|
||||
* Explicitly initialized so library localization can demote it: a tentative
|
||||
* definition is a COMMON symbol on Mach-O, and COMMON symbols stay global. */
|
||||
ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES] = {0};
|
||||
static uint32_t scr_cyc_ntypes = 1;
|
||||
|
||||
/* Assign `type` its id on its first allocation; any thread may get there
|
||||
|
||||
@@ -96,8 +96,10 @@ typedef struct ScrSidx {
|
||||
/* Keep short-string cursor traffic out of the sparse cache. A short-lived
|
||||
* one-byte receiver can be far more common than a large indexed one; sharing
|
||||
* the round-robin slots would otherwise rebuild a warm index every few calls.
|
||||
* Explicitly initialized so library localization can demote it: a tentative
|
||||
* definition is a COMMON symbol on Mach-O, and COMMON symbols stay global.
|
||||
*/
|
||||
SCR_TL ScrStrScan scr_str_scan;
|
||||
SCR_TL ScrStrScan scr_str_scan = {0};
|
||||
static SCR_TL ScrSidx scr_sidx_sparse_tab[SCR_SIDX_N];
|
||||
static SCR_TL ScrSidx scr_sidx_cursor_tab[SCR_SIDX_N];
|
||||
static SCR_TL unsigned scr_sidx_sparse_clock;
|
||||
@@ -936,7 +938,8 @@ static inline uint32_t scr_utf8_decode(const char *p, size_t *adv) {
|
||||
|
||||
/* ── WTF-8 lone surrogates (scr_runtime.h) ────────────────────────── */
|
||||
|
||||
bool scr_wtf8_lone;
|
||||
/* Explicitly initialized for library localization (see scr_str_scan). */
|
||||
bool scr_wtf8_lone = false;
|
||||
|
||||
void scr_wtf8_note(void) {
|
||||
if (!scr_wtf8_any()) __atomic_store_n(&scr_wtf8_lone, true, __ATOMIC_RELAXED);
|
||||
@@ -2603,6 +2606,25 @@ static int scr_digit_value(char c) {
|
||||
return 99;
|
||||
}
|
||||
|
||||
/* One fold of V8's generic-radix loop: v * multiplier + part. Node's V8 is
|
||||
* compiled with the C++ compiler's default floating-point contraction,
|
||||
* which fuses this multiply-add into one rounding on arm64 and keeps two
|
||||
* roundings on x86-64, so Node answers long inputs differently on the two
|
||||
* architectures (parseInt("9007199254740993", 36) is 1.9896986116031812e+24
|
||||
* on arm64 and 1.9896986116031807e+24 on x64). Each form is written out so
|
||||
* the result matches the same architecture's Node under any contraction
|
||||
* setting this file is compiled with. */
|
||||
static inline double scr_radix_fold(double v, uint32_t multiplier, uint32_t part) {
|
||||
#if defined(__aarch64__)
|
||||
return fma(v, (double)multiplier, (double)part);
|
||||
#else
|
||||
#ifdef __clang__
|
||||
#pragma clang fp contract(off)
|
||||
#endif
|
||||
return v * (double)multiplier + (double)part;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Digit string → double (digits already validated against the radix,
|
||||
* leading zeros stripped, count >= 1). Node (V8) is the oracle, and V8 is
|
||||
* only EXACT where the spec requires it (radix 2, 4, 8, 10, 16, 32); for
|
||||
@@ -2615,8 +2637,8 @@ static double scr_digits_to_double(const char *p, size_t n, int radix) {
|
||||
if (radix != 10 && (radix & (radix - 1)) != 0) {
|
||||
/* V8's generic-radix loop (numbers/conversions.cc): consume the
|
||||
* longest digit run whose multiplier stays below 0xFFFFFFFF/36, then
|
||||
* fold with one double multiply-add per chunk — the rounding of each
|
||||
* fold IS the observable Node behavior. */
|
||||
* fold with one double multiply-add per chunk (scr_radix_fold) — the
|
||||
* rounding of each fold IS the observable Node behavior. */
|
||||
const uint32_t kMaximumMultiplier = 0xFFFFFFFFu / 36u;
|
||||
double v = 0.0;
|
||||
size_t i = 0;
|
||||
@@ -2635,7 +2657,7 @@ static double scr_digits_to_double(const char *p, size_t n, int radix) {
|
||||
multiplier = m;
|
||||
i++;
|
||||
}
|
||||
v = v * (double)multiplier + (double)part;
|
||||
v = scr_radix_fold(v, multiplier, part);
|
||||
} while (!done);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, readdir, readFile } from "node:fs/promises";
|
||||
import { availableParallelism } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const testDir = import.meta.dirname;
|
||||
const srcDir = join(testDir, "../src");
|
||||
const vendorDir = join(testDir, "../vendor");
|
||||
const buildDir = join(testDir, "build", "common-symbols");
|
||||
|
||||
// Library localization (abi.localize_runtime) keeps COMMON symbols global,
|
||||
// so a runtime global written as a tentative definition (`T x;` at file
|
||||
// scope with no initializer) is exported from every localized library
|
||||
// archive on toolchains that emit tentative definitions as COMMON symbols,
|
||||
// such as the macOS runtime build. Linux clang defaults to -fno-common, where
|
||||
// the same declaration is an ordinary definition, so the library-multi
|
||||
// symbol checks only catch it on macOS. This test compiles every runtime
|
||||
// source with -fcommon, in the plain and worker configurations, and expects
|
||||
// no COMMON symbols: every external runtime global needs an initializer.
|
||||
test("runtime globals are not tentative definitions", async () => {
|
||||
await mkdir(buildDir, { recursive: true });
|
||||
const sources = (await readdir(srcDir))
|
||||
.filter((name) => name.endsWith(".c") && !name.endsWith(".test.c"))
|
||||
.sort();
|
||||
const jobs = sources.flatMap((name) =>
|
||||
[[], ["-DSCR_WORKERS"]].map((defines) => ({ name, defines })),
|
||||
);
|
||||
const common: string[] = [];
|
||||
let next = 0;
|
||||
const worker = async (): Promise<void> => {
|
||||
while (next < jobs.length) {
|
||||
const { name, defines } = jobs[next++]!;
|
||||
const output = join(buildDir, `${name}${defines.join("")}.ll`);
|
||||
await execFileAsync("clang", [
|
||||
"-std=c11",
|
||||
"-O0",
|
||||
"-fcommon",
|
||||
"-S",
|
||||
"-emit-llvm",
|
||||
...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
|
||||
...defines,
|
||||
`-I${join(vendorDir, "quickjs-ng")}`,
|
||||
`-I${join(vendorDir, "mbedtls", "include")}`,
|
||||
"-o",
|
||||
output,
|
||||
join(srcDir, name),
|
||||
]);
|
||||
for (const match of (await readFile(output, "utf8")).matchAll(/^@(\S+) = common /gm)) {
|
||||
common.push(`${name}${defines.length > 0 ? ` (${defines.join(" ")})` : ""}: ${match[1]}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.min(8, availableParallelism()) }, worker));
|
||||
expect(common.sort()).toEqual([]);
|
||||
});
|
||||
@@ -309,8 +309,42 @@ const radices = [
|
||||
-4294967280,
|
||||
35,
|
||||
];
|
||||
// parseInt cases whose Node answer depends on the architecture, keyed by
|
||||
// input and radix argument. For radixes other than 2, 4, 8, 10, 16 and 32, V8
|
||||
// folds each 32-bit digit chunk with `value * multiplier + part`; Node's
|
||||
// arm64 builds fuse that multiply-add into one rounding and its x64 builds
|
||||
// round twice. Each case is written once per architecture with the listed
|
||||
// answers, so the file is the same whichever machine generates it, and the
|
||||
// answer for this machine's architecture is checked against Node here.
|
||||
const ARCH_PARSEINT = new Map([
|
||||
["9007199254740993\t36", { arm64: "1.9896986116031812e+24", x64: "1.9896986116031807e+24" }],
|
||||
[
|
||||
"123456789012345678901234567890\t36",
|
||||
{ arm64: "1.436287679432363e+45", x64: "1.4362876794323633e+45" },
|
||||
],
|
||||
]);
|
||||
const ownArch = process.arch === "arm64" ? "arm64" : "x64";
|
||||
const archCases = new Set();
|
||||
for (const s of parseIntInputs)
|
||||
for (const r of radices)
|
||||
emit("parseInt", s, r === undefined ? "0" : num(r), hex(String(parseInt(s, r))));
|
||||
for (const r of radices) {
|
||||
const args = r === undefined ? "0" : num(r);
|
||||
const answer = String(parseInt(s, r));
|
||||
const arch = ARCH_PARSEINT.get(`${s}\t${args}`);
|
||||
if (arch === undefined) {
|
||||
emit("parseInt", s, args, hex(answer));
|
||||
continue;
|
||||
}
|
||||
if (answer !== arch[ownArch]) {
|
||||
throw new Error(
|
||||
`parseInt(${JSON.stringify(s)}, ${args}) is ${answer} on ${process.arch}; ARCH_PARSEINT lists ${arch[ownArch]}`,
|
||||
);
|
||||
}
|
||||
archCases.add(`${s}\t${args}`);
|
||||
emit("parseInt.arm64", s, args, hex(arch.arm64));
|
||||
emit("parseInt.x64", s, args, hex(arch.x64));
|
||||
}
|
||||
if (archCases.size !== ARCH_PARSEINT.size) {
|
||||
throw new Error("ARCH_PARSEINT lists a case that parseIntInputs and radices do not generate");
|
||||
}
|
||||
|
||||
stdout.write([...lines].join("\n") + "\n");
|
||||
|
||||
@@ -34282,7 +34282,8 @@ parseInt 39303037313939323534373430393933 2 4e614e
|
||||
parseInt 39303037313939323534373430393933 8 4e614e
|
||||
parseInt 39303037313939323534373430393933 10 39303037313939323534373430393932
|
||||
parseInt 39303037313939323534373430393933 16 3130333738323931393832353731343037303030
|
||||
parseInt 39303037313939323534373430393933 36 312e39383936393836313136303331383037652b3234
|
||||
parseInt.arm64 39303037313939323534373430393933 36 312e39383936393836313136303331383132652b3234
|
||||
parseInt.x64 39303037313939323534373430393933 36 312e39383936393836313136303331383037652b3234
|
||||
parseInt 39303037313939323534373430393933 1 4e614e
|
||||
parseInt 39303037313939323534373430393933 37 4e614e
|
||||
parseInt 39303037313939323534373430393933 -1 4e614e
|
||||
@@ -34316,7 +34317,8 @@ parseInt 313233343536373839303132333435363738393031323334353637383930 2 31
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 8 333432333931
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 10 312e32333435363738393031323334353638652b3239
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 16 392e343532323837393638373336353437652b3334
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 36 312e34333632383736373934333233363333652b3435
|
||||
parseInt.arm64 313233343536373839303132333435363738393031323334353637383930 36 312e343336323837363739343332333633652b3435
|
||||
parseInt.x64 313233343536373839303132333435363738393031323334353637383930 36 312e34333632383736373934333233363333652b3435
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 1 4e614e
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 37 4e614e
|
||||
parseInt 313233343536373839303132333435363738393031323334353637383930 -1 4e614e
|
||||
|
||||
@@ -53,37 +53,17 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
// Runs against the committed case file generated from Node via
|
||||
// gen-string-cases.mjs. The test below also verifies the checked-in oracle
|
||||
// against the generator, so an accidental stale result cannot be masked by
|
||||
// the platform-specific native-runtime allowlist. Covers UTF-16
|
||||
// gen-string-cases.mjs. The first test below also verifies the checked-in
|
||||
// oracle against the generator, so a stale result cannot pass. Covers UTF-16
|
||||
// length/charCodeAt/indexOf/includes/startsWith/endsWith/slice/repeat/
|
||||
// trim/trimStart/trimEnd/split/padStart/padEnd/charAt plus parseInt over
|
||||
// ASCII, Latin-1, CJK, astral, and combining-mark strings, including the
|
||||
// documented lone-surrogate → U+FFFD divergence.
|
||||
//
|
||||
// On macOS every committed case matches exactly. On Linux (glibc,
|
||||
// measured on the sandbox-suite lane's AL2023 clang 15) exactly two
|
||||
// parseInt cases — giant base-36 inputs whose magnitude assembly rides
|
||||
// libm — land one ulp away from V8's correctly-rounded conversion.
|
||||
// scr_number.c is pinned and shared across every consumer, so the
|
||||
// divergence is documented HERE as an allowlist keyed by exact input and
|
||||
// exact divergent output: a third value, a new failing case, or a future
|
||||
// conversion fix all make the test fail.
|
||||
const LINUX_PARSEINT_ULP: ReadonlyMap<string, string> = new Map([
|
||||
[
|
||||
// parseInt("9007199254740993", 36): Node 1.9896986116031812e+24,
|
||||
// glibc lane 1.9896986116031807e+24.
|
||||
"parseInt(39303037313939323534373430393933 ; 36)",
|
||||
"312e39383936393836313136303331383037652b3234",
|
||||
],
|
||||
[
|
||||
// parseInt("123456789012345678901234567890", 36):
|
||||
// Node 1.436287679432363e+45, glibc lane 1.4362876794323633e+45.
|
||||
"parseInt(313233343536373839303132333435363738393031323334353637383930 ; 36)",
|
||||
"312e34333632383736373934333233363333652b3435",
|
||||
],
|
||||
]);
|
||||
|
||||
// parseInt cases whose Node answer differs between arm64 and x64 are
|
||||
// recorded once per architecture ("parseInt.arm64" / "parseInt.x64", see
|
||||
// ARCH_PARSEINT in gen-string-cases.mjs), so the generated file is the same
|
||||
// on either machine and the C harness checks this architecture's answer.
|
||||
test("committed string oracle matches Node", async () => {
|
||||
const caseFile = join(testDir, "string-cases.txt");
|
||||
const [{ stdout }, committed] = await Promise.all([
|
||||
@@ -92,18 +72,10 @@ test("committed string oracle matches Node", async () => {
|
||||
}),
|
||||
readFile(caseFile, "utf8"),
|
||||
]);
|
||||
// The committed cases come from Node on arm64. Node on x64 answers the two
|
||||
// allowlisted base-36 parseInt cases with exactly the values listed above;
|
||||
// every other line, and any other value for those two, must still match.
|
||||
const generated = stdout.split("\n");
|
||||
const expected = committed.split("\n");
|
||||
expect(generated).toHaveLength(expected.length);
|
||||
const differing = generated.filter((line, index) => line !== expected[index]);
|
||||
if (process.arch === "arm64") expect(differing).toEqual([]);
|
||||
for (const line of differing) {
|
||||
const [operation, input, radix, output] = line.split("\t");
|
||||
expect(LINUX_PARSEINT_ULP.get(`${operation}(${input} ; ${radix})`), line).toBe(output);
|
||||
}
|
||||
expect(generated.filter((line, index) => line !== expected[index])).toEqual([]);
|
||||
});
|
||||
|
||||
test("string methods match Node on committed oracle cases", async () => {
|
||||
@@ -111,25 +83,7 @@ test("string methods match Node on committed oracle cases", async () => {
|
||||
(v) => ({ stderr: v.stderr }),
|
||||
(e: Error & { stderr?: string }) => ({ stderr: e.stderr ?? "" }),
|
||||
);
|
||||
const stderr = r.stderr.trim();
|
||||
if (process.platform !== "linux") {
|
||||
expect(stderr).toMatch(/^(\d+)\/\1 cases passed$/);
|
||||
return;
|
||||
}
|
||||
const lines = stderr.split("\n");
|
||||
const mismatches = lines.filter((l) => l.startsWith("MISMATCH "));
|
||||
for (const line of mismatches) {
|
||||
const m = /^MISMATCH (.+) expected=[0-9a-f]* got=([0-9a-f]*)$/.exec(line);
|
||||
expect(m, `unparseable mismatch line: ${line}`).not.toBeNull();
|
||||
expect(LINUX_PARSEINT_ULP.get(m![1]!), line).toBe(m![2]);
|
||||
}
|
||||
// The harness prints at most 20 mismatch lines; the pass-count tail is
|
||||
// the ground truth that NOTHING beyond the printed (allowlisted) set
|
||||
// failed.
|
||||
const t = /^(\d+)\/(\d+) cases passed$/.exec(lines.at(-1) ?? "");
|
||||
expect(t, `missing pass-count line: ${lines.at(-1)}`).not.toBeNull();
|
||||
expect(Number(t![2]) - Number(t![1]), "failed-case count").toBe(mismatches.length);
|
||||
expect(mismatches.length).toBeLessThanOrEqual(LINUX_PARSEINT_ULP.size);
|
||||
expect(r.stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
|
||||
});
|
||||
|
||||
// repeat(count) with count < 0 or Infinity is Node's catchable RangeError
|
||||
|
||||
@@ -27,6 +27,16 @@
|
||||
long scr_str_live_count(void); /* provided by scr_string.c */
|
||||
#endif
|
||||
|
||||
/* parseInt cases whose Node answer depends on the architecture carry it in
|
||||
* the op name ("parseInt.arm64" / "parseInt.x64"); only this build's runs.
|
||||
* scr_radix_fold uses the fused multiply-add on aarch64 and two roundings
|
||||
* elsewhere, matching Node on arm64 and x64. */
|
||||
#if defined(__aarch64__)
|
||||
#define SCR_TEST_PARSEINT_ARCH "parseInt.arm64"
|
||||
#else
|
||||
#define SCR_TEST_PARSEINT_ARCH "parseInt.x64"
|
||||
#endif
|
||||
|
||||
#ifdef SCR_SIDX_TEST
|
||||
/* Test-only sparse-index observability, deliberately absent from the normal
|
||||
* runtime ABI. The walker count is code-point steps after a cache prime. */
|
||||
@@ -977,9 +987,12 @@ int main(int argc, char **argv) {
|
||||
} else if (strcmp(op, "trimEnd") == 0) {
|
||||
check_str(op, args, input, scr_str_trim_end(input), expected_bytes,
|
||||
exp_len);
|
||||
} else if (strcmp(op, "parseInt") == 0) {
|
||||
} else if (strcmp(op, "parseInt") == 0 ||
|
||||
strcmp(op, SCR_TEST_PARSEINT_ARCH) == 0) {
|
||||
check_f64(op, args, input, scr_parse_int(input, strtod(args, NULL)),
|
||||
expected_bytes, exp_len);
|
||||
} else if (strncmp(op, "parseInt.", 9) == 0) {
|
||||
/* another architecture's answer (see SCR_TEST_PARSEINT_ARCH) */
|
||||
} else if (strcmp(op, "split") == 0 || strcmp(op, "splitLimit") == 0) {
|
||||
/* args = separator hex; expected = "<count>:<pieces joined by 0x01>" */
|
||||
double limit = 4294967295.0;
|
||||
|
||||
Reference in New Issue
Block a user