mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
The native compiler ran four large module initializers at every startup: the builtin call signatures (LIB_FN_SIGS), the Node 24 fetch compatibility profile, and the builtin module function and ambient surface tables. Together their init functions are about 1.3 MB of code, and --version and warm cache-hit builds paged them in and built the tables without reading them. Each table is now built by a function on its first call and kept in a module variable: - libFnSignatures() replaces LIB_FN_SIGS - node24FetchCompatProfile() and staticFetchMembers() replace NODE24_FETCH_COMPAT_PROFILE and the STATIC_* member sets - builtinModuleFns() and ambientSurfaceFns() replace BUILTIN_MODULE_FNS and AMBIENT_SURFACE_FNS The @scriptc/compiler entry point keeps LIB_FN_SIGS and NODE24_FETCH_COMPAT_PROFILE as deprecated constants built from the new functions when the entry module loads. The native compiler does not load that module. The surface manifest's coverage notes read the fetch target through the profile function, so building the manifest module no longer builds the profile. The table contents are unchanged.
397 lines
14 KiB
TypeScript
397 lines
14 KiB
TypeScript
/**
|
|
* Generated, version-pinned conformance for the engine-free fetch slice.
|
|
*
|
|
* The compatibility profile is compiler input, not test-only metadata: its
|
|
* member allowlists drive lowering and its entries project into the shipped
|
|
* surface manifest. The reflected inventory supplies the denominator. This
|
|
* suite checks that the pinned Node executable is the intended oracle, every
|
|
* profile row names differential evidence, and the generated
|
|
* WebIDL/state-machine program agrees through LLVM.
|
|
*/
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { beforeAll, describe, expect, test } from "vitest";
|
|
import {
|
|
compile,
|
|
node24FetchCompatProfile,
|
|
renderDiagnostics,
|
|
type FetchCompatEvidence,
|
|
} from "@scriptc/compiler";
|
|
import {
|
|
FETCH_CONFORMANCE_SCENARIOS,
|
|
FETCH_CONFORMANCE_SEED,
|
|
generateFetchConformanceProgram,
|
|
generatedScenarioIds,
|
|
} from "./fetch-conformance-program.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = join(import.meta.dirname, "../..");
|
|
const fixturesRoot = join(repoRoot, "tests/fixtures/fetch");
|
|
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
|
const profile = node24FetchCompatProfile();
|
|
function configuredInteger(name: string, fallback: number, maximum: number): number {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === "") return fallback;
|
|
const value = Number(raw);
|
|
if (!Number.isInteger(value) || value < 0 || value > maximum) {
|
|
throw new Error(`${name} must be an integer between 0 and ${maximum}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const conformanceSeed = configuredInteger(
|
|
"SCRIPTC_FETCH_CONFORMANCE_SEED",
|
|
FETCH_CONFORMANCE_SEED,
|
|
0xffff_ffff,
|
|
);
|
|
const conformanceTraceCount = configuredInteger("SCRIPTC_FETCH_CONFORMANCE_TRACES", 12, 100);
|
|
const generatedSource = generateFetchConformanceProgram(profile, {
|
|
seed: conformanceSeed,
|
|
traceCount: conformanceTraceCount,
|
|
});
|
|
const sourceHash = createHash("sha256").update(generatedSource).digest("hex").slice(0, 16);
|
|
const workRoot = mkdtempSync(join(tmpdir(), "scriptc-fetch-conformance-"));
|
|
const entry = join(workRoot, "main.js");
|
|
|
|
interface RunResult {
|
|
stdout: Buffer;
|
|
stderr: Buffer;
|
|
exitCode: number;
|
|
}
|
|
|
|
function normalizedStderr(result: RunResult): string {
|
|
const stderr = result.stderr.toString("utf8");
|
|
if (!sanitize) return stderr;
|
|
// Linux ASan prints this once per process at the runtime's first fiber
|
|
// context switch. It has no suppression switch and is harness noise, not
|
|
// program stderr; retain every other byte so real sanitizer reports fail.
|
|
return stderr.replace(
|
|
/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext.*\n/gm,
|
|
"",
|
|
);
|
|
}
|
|
|
|
async function run(command: string, args: string[]): Promise<RunResult> {
|
|
try {
|
|
const result = await execFileAsync(command, args, {
|
|
encoding: "buffer",
|
|
env: process.env,
|
|
timeout: 30_000,
|
|
});
|
|
return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
|
|
} catch (error) {
|
|
const failure = error as {
|
|
code?: unknown;
|
|
stdout?: Buffer;
|
|
stderr?: Buffer;
|
|
};
|
|
if (
|
|
typeof failure.code !== "number" ||
|
|
!Buffer.isBuffer(failure.stdout) ||
|
|
!Buffer.isBuffer(failure.stderr)
|
|
) {
|
|
throw error;
|
|
}
|
|
return {
|
|
stdout: failure.stdout,
|
|
stderr: failure.stderr,
|
|
exitCode: failure.code,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function build(backend: "llvm"): Promise<string> {
|
|
const outDir = join(workRoot, `${sourceHash}-${backend}-${sanitize ? "san" : "plain"}`);
|
|
mkdirSync(outDir, { recursive: true });
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: join(outDir, "program"),
|
|
backend,
|
|
sanitize,
|
|
});
|
|
if (!result.ok) {
|
|
expect.unreachable(
|
|
`generated fetch conformance program failed to compile (${backend}):\n` +
|
|
renderDiagnostics(result.diagnostics, result.sourceTexts, { color: false }) +
|
|
`\ngenerated source: ${entry}`,
|
|
);
|
|
}
|
|
return result.binaryPath;
|
|
}
|
|
|
|
function evidenceKey(evidence: FetchCompatEvidence): string {
|
|
if (evidence.generated !== undefined && evidence.fixture === undefined) {
|
|
return `generated:${evidence.generated}`;
|
|
}
|
|
if (evidence.fixture !== undefined && evidence.generated === undefined) {
|
|
return `fixture:${evidence.fixture}`;
|
|
}
|
|
throw new Error("fetch profile evidence must name exactly one source");
|
|
}
|
|
|
|
interface RuntimeInterfaceSurface {
|
|
statics: string[];
|
|
prototype: string[];
|
|
inherited: string[];
|
|
symbols: string[];
|
|
}
|
|
|
|
const publicWellKnownSymbols = new Map<symbol, string>();
|
|
for (const name of Object.getOwnPropertyNames(Symbol)) {
|
|
const value = (Symbol as unknown as Record<string, unknown>)[name];
|
|
if (typeof value === "symbol") {
|
|
publicWellKnownSymbols.set(value, `[Symbol.${name}]`);
|
|
}
|
|
}
|
|
|
|
function publicSymbolName(symbol: symbol): string | null {
|
|
const publicName = publicWellKnownSymbols.get(symbol);
|
|
if (publicName !== undefined) return publicName;
|
|
// Node-private transfer/inspection symbols are implementation details,
|
|
// not the public WebIDL surface this profile promises to classify.
|
|
return null;
|
|
}
|
|
|
|
function runtimeInterfaceSurface(name: string): RuntimeInterfaceSurface {
|
|
const value = (globalThis as unknown as Record<string, unknown>)[name];
|
|
expect(typeof value, `${name} must be a global constructor object`).toBe("function");
|
|
const ctor = value as Function & { prototype: object };
|
|
const prototype = ctor.prototype;
|
|
const ownNames = Object.getOwnPropertyNames(prototype).filter(
|
|
(member) => member !== "constructor",
|
|
);
|
|
const inherited: string[] = [];
|
|
const visibleNames = new Set(ownNames);
|
|
const symbols: string[] = [];
|
|
const visibleSymbols = new Set<symbol>();
|
|
|
|
for (
|
|
let current: object | null = prototype;
|
|
current !== null && current !== Object.prototype;
|
|
current = Object.getPrototypeOf(current) as object | null
|
|
) {
|
|
if (current !== prototype) {
|
|
for (const member of Object.getOwnPropertyNames(current)) {
|
|
if (member === "constructor" || visibleNames.has(member)) continue;
|
|
visibleNames.add(member);
|
|
inherited.push(member);
|
|
}
|
|
}
|
|
for (const symbol of Object.getOwnPropertySymbols(current)) {
|
|
if (visibleSymbols.has(symbol)) continue;
|
|
visibleSymbols.add(symbol);
|
|
const member = publicSymbolName(symbol);
|
|
if (member !== null) symbols.push(member);
|
|
}
|
|
}
|
|
|
|
return {
|
|
statics: Object.getOwnPropertyNames(ctor).filter(
|
|
(member) => !["length", "name", "prototype"].includes(member),
|
|
),
|
|
prototype: ownNames,
|
|
inherited,
|
|
symbols,
|
|
};
|
|
}
|
|
|
|
function dictionaryReads(construct: (init: object) => unknown): string[] {
|
|
const reads: string[] = [];
|
|
const init = new Proxy(
|
|
{},
|
|
{
|
|
get(_target, key) {
|
|
reads.push(String(key));
|
|
return undefined;
|
|
},
|
|
},
|
|
);
|
|
construct(init);
|
|
return [...new Set(reads)];
|
|
}
|
|
|
|
beforeAll(() => {
|
|
writeFileSync(entry, generatedSource);
|
|
});
|
|
|
|
describe("Node 24 fetch compatibility profile", () => {
|
|
test("the running oracle is the exact pinned Node/Undici tuple", () => {
|
|
const pinnedNode = readFileSync(join(repoRoot, ".node-version"), "utf8").trim();
|
|
expect(profile.target.node).toBe(pinnedNode);
|
|
expect(process.versions.node).toBe(profile.target.node);
|
|
expect(process.versions.undici).toBe(profile.target.undici);
|
|
});
|
|
|
|
test("the inventory classifies every supported row and every gap", () => {
|
|
const supported = [...profile.operations, ...profile.requestInit, ...profile.responseInit]
|
|
.map((row) => row.id)
|
|
.sort();
|
|
const entries = profile.inventory.entries;
|
|
const ids = entries.map((entry) => entry.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
expect(
|
|
entries
|
|
.filter((entry) => entry.status === "static")
|
|
.map((entry) => entry.id)
|
|
.sort(),
|
|
).toEqual(supported);
|
|
|
|
for (const entry of entries) {
|
|
if (entry.status === "static") {
|
|
expect(entry.code, `${entry.id}: static rows have no refusal code`).toBeUndefined();
|
|
expect(entry.reason, `${entry.id}: static rows are explained by evidence`).toBeUndefined();
|
|
} else if (entry.status === "unsupported") {
|
|
expect(entry.code, `${entry.id}: non-static rows use the stdlib fence`).toBe("SC2020");
|
|
expect(entry.reason?.length, `${entry.id}: missing gap rationale`).toBeGreaterThan(0);
|
|
} else {
|
|
expect(entry.code, `${entry.id}: exclusions are not refusal claims`).toBeUndefined();
|
|
expect(entry.reason?.length, `${entry.id}: missing scope rationale`).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
|
|
expect(entries.some((entry) => entry.status === "unsupported")).toBe(true);
|
|
expect(entries.some((entry) => entry.status === "out-of-scope")).toBe(true);
|
|
expect(profile.inventory.excludedInterfaces.length).toBeGreaterThan(0);
|
|
for (const exclusion of profile.inventory.excludedInterfaces) {
|
|
expect(exclusion.name.length).toBeGreaterThan(0);
|
|
expect(exclusion.reason.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("the selected runtime interfaces match the complete public census", () => {
|
|
const entries = profile.inventory.entries;
|
|
const expected = (owner: string, placement: string): string[] =>
|
|
entries
|
|
.filter((entry) => entry.owner === owner && entry.placement === placement)
|
|
.map((entry) => entry.member)
|
|
.sort();
|
|
|
|
for (const owner of profile.inventory.interfaces) {
|
|
expect(
|
|
entries.filter((entry) => entry.owner === owner && entry.placement === "constructor")
|
|
.length,
|
|
`${owner}: constructor classification`,
|
|
).toBe(1);
|
|
const actual = runtimeInterfaceSurface(owner);
|
|
expect(actual.statics.sort(), `${owner}: static members`).toEqual(expected(owner, "static"));
|
|
expect(actual.prototype.sort(), `${owner}: own prototype members`).toEqual(
|
|
expected(owner, "prototype"),
|
|
);
|
|
expect(actual.inherited.sort(), `${owner}: inherited prototype members`).toEqual(
|
|
expected(owner, "prototype-inherited"),
|
|
);
|
|
expect(actual.symbols.sort(), `${owner}: public symbol members`).toEqual(
|
|
expected(owner, "prototype-symbol"),
|
|
);
|
|
}
|
|
expect(typeof globalThis.fetch).toBe("function");
|
|
});
|
|
|
|
test("the public-symbol classifier covers every well-known symbol", () => {
|
|
expect(publicWellKnownSymbols.size).toBeGreaterThan(0);
|
|
for (const [symbol, name] of publicWellKnownSymbols) {
|
|
expect(publicSymbolName(symbol)).toBe(name);
|
|
}
|
|
expect(publicSymbolName(Symbol("nodejs.private"))).toBeNull();
|
|
});
|
|
|
|
test("the WebIDL dictionary census matches Node's conversion reads", () => {
|
|
const expected = (owner: string): string[] =>
|
|
profile.inventory.entries
|
|
.filter((entry) => entry.owner === owner && entry.placement === "dictionary")
|
|
.map((entry) => entry.member);
|
|
expect(
|
|
dictionaryReads((init) => new Request("http://example.com", init as RequestInit)),
|
|
).toEqual(expected("RequestInit"));
|
|
expect(dictionaryReads((init) => new Response(null, init as ResponseInit))).toEqual(
|
|
expected("ResponseInit"),
|
|
);
|
|
});
|
|
|
|
test("every row has unique ids and resolvable differential evidence", () => {
|
|
expect(profile.schemaVersion).toBe(1);
|
|
const rows = [...profile.operations, ...profile.requestInit, ...profile.responseInit];
|
|
const ids = rows.map((row) => row.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
expect(profile.operations.length).toBeGreaterThanOrEqual(35);
|
|
|
|
for (const row of rows) {
|
|
expect(row.evidence.length, `${row.id}: missing differential evidence`).toBeGreaterThan(0);
|
|
for (const evidence of row.evidence) {
|
|
const key = evidenceKey(evidence);
|
|
if (evidence.generated !== undefined) {
|
|
expect(
|
|
FETCH_CONFORMANCE_SCENARIOS,
|
|
`${row.id}: unknown generated scenario ${key}`,
|
|
).toContain(evidence.generated);
|
|
} else {
|
|
const root = join(fixturesRoot, evidence.fixture!);
|
|
expect(
|
|
existsSync(join(root, "main.js")) || existsSync(join(root, "main.mts")),
|
|
`${row.id}: missing ${key}`,
|
|
).toBe(true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("the profile member allowlists have matching operation rows", () => {
|
|
for (const member of [...profile.members.responseReads, ...profile.members.responseCalls]) {
|
|
expect(
|
|
profile.operations.some((operation) => operation.name === `Response.${member}`),
|
|
`Response.${member} has no compatibility row`,
|
|
).toBe(true);
|
|
}
|
|
for (const member of [
|
|
...profile.members.readableStreamReads,
|
|
...profile.members.readableStreamCalls,
|
|
]) {
|
|
expect(
|
|
profile.operations.some((operation) => operation.name === `ReadableStream.${member}`),
|
|
`ReadableStream.${member} has no compatibility row`,
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("generation is deterministic and covers every registered scenario", () => {
|
|
expect(
|
|
generateFetchConformanceProgram(profile, {
|
|
seed: conformanceSeed,
|
|
traceCount: conformanceTraceCount,
|
|
}),
|
|
).toBe(generatedSource);
|
|
expect(generatedScenarioIds(profile)).toEqual([...FETCH_CONFORMANCE_SCENARIOS].sort());
|
|
for (const scenario of FETCH_CONFORMANCE_SCENARIOS) {
|
|
expect(generatedSource).toContain(`// scenario: ${scenario}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe(
|
|
`generated fetch conformance (seed=${conformanceSeed}, traces=${conformanceTraceCount}` +
|
|
`${sanitize ? ", sanitized" : ""})`,
|
|
() => {
|
|
test.for(["llvm"] as const)(
|
|
"%s backend matches the pinned Node oracle",
|
|
async (backend) => {
|
|
const binary = await build(backend);
|
|
const [nodeResult, nativeResult] = await Promise.all([
|
|
run(process.execPath, [entry]),
|
|
run(binary, []),
|
|
]);
|
|
if (!nativeResult.stdout.equals(nodeResult.stdout)) {
|
|
expect(nativeResult.stdout.toString("utf8")).toBe(nodeResult.stdout.toString("utf8"));
|
|
expect.unreachable("stdout differed at byte level but not after UTF-8 decoding");
|
|
}
|
|
expect(normalizedStderr(nativeResult)).toBe(nodeResult.stderr.toString("utf8"));
|
|
expect(nativeResult.exitCode).toBe(nodeResult.exitCode);
|
|
},
|
|
120_000,
|
|
);
|
|
},
|
|
);
|