Files
scriptc/tests/harness/fetch-conformance-program.ts
T
Chris Tate 9abf35f6a2 Build the large compiler tables on first use
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.
2026-10-25 05:23:35 -05:00

444 lines
12 KiB
TypeScript

import type { FetchCompatProfile } from "../../packages/compiler/src/compat/fetch-profile.js";
export const FETCH_CONFORMANCE_SEED = 0x24_15_07_24;
export const FETCH_CONFORMANCE_SCENARIOS = [
"abort-events",
"stream-traces",
"webidl-operations",
] as const;
type GeneratedScenario = (typeof FETCH_CONFORMANCE_SCENARIOS)[number];
class XorShift32 {
constructor(private state: number) {
if (state === 0) this.state = 0x9e3779b9;
}
next(): number {
let value = this.state | 0;
value ^= value << 13;
value ^= value >>> 17;
value ^= value << 5;
this.state = value | 0;
return value >>> 0;
}
pick(size: number): number {
return this.next() % size;
}
}
function webIdlOperations(): string {
return String.raw`
let webIdlEffects = "";
function webIdlEffect(label) {
webIdlEffects += label;
return "ignored";
}
const abortReason = JSON.parse('{"value":"before"}');
const abortedSignal = AbortSignal.abort(
abortReason,
webIdlEffect("abort "),
);
abortReason.value = "after";
console.log(
"webidl abort:",
webIdlEffects,
abortedSignal.aborted,
abortedSignal.reason.value,
);
const combinedSignal = AbortSignal.any(
[abortedSignal],
webIdlEffect("any "),
);
console.log(
"webidl any:",
webIdlEffects,
combinedSignal.aborted,
combinedSignal.reason.value,
);
try {
combinedSignal.throwIfAborted();
console.log("webidl throwIfAborted: no-error");
} catch (error) {
console.log(
"webidl throwIfAborted:",
error.value,
);
}
webIdlEffects = "";
try {
AbortSignal.timeout(
JSON.parse('{"delay":1}'),
webIdlEffect("timeout-surplus"),
);
console.log("webidl invalid timeout: no-error");
} catch (error) {
console.log(
"webidl invalid timeout:",
webIdlEffects,
error.name,
error.code || "no-code",
error.message,
);
}
try {
AbortSignal.timeout();
console.log("webidl missing timeout: no-error");
} catch (error) {
console.log(
"webidl missing timeout:",
error.name,
error.code || "no-code",
error.message,
);
}
try {
AbortSignal.any();
console.log("webidl missing any: no-error");
} catch (error) {
console.log(
"webidl missing any:",
error.name,
error.code || "no-code",
error.message,
);
}
try {
AbortSignal.any(JSON.parse('{"not":"a sequence"}'));
console.log("webidl invalid any: no-error");
} catch (error) {
console.log(
"webidl invalid any:",
error.name,
error.code || "no-code",
error.message,
);
}
try {
ReadableStream.from();
console.log("webidl missing from: no-error");
} catch (error) {
console.log(
"webidl missing from:",
error.name,
error.code || "no-code",
error.message,
);
}
const fromItem = { value: "before" };
const fromItems = [fromItem];
webIdlEffects = "";
const fromStream = ReadableStream.from(
fromItems,
webIdlEffect("from-surplus"),
);
fromItems[0].value = "after";
const fromReader = fromStream.getReader();
const fromPart = await fromReader.read();
const fromDone = await fromReader.read();
console.log(
"webidl from:",
webIdlEffects,
fromPart.done,
fromPart.done ? "done" : fromPart.value.value,
fromDone.done,
fromStream.locked,
);
await fromReader.closed;
`;
}
function abortEvents(): string {
return String.raw`
let abortEventOrder = "";
let abortEventNameEffects = "";
const eventSignal = AbortSignal.timeout(0);
function abortEventNameToString() {
abortEventNameEffects += this.operation;
return "abort";
}
function keptAbortListener(event) {
abortEventOrder +=
this === eventSignal &&
event.target === eventSignal &&
event.currentTarget === eventSignal
? "listener-this "
: "listener-wrong ";
}
function removedAbortListener() {
abortEventOrder += "removed ";
}
function onAbortListener(event) {
abortEventOrder +=
this === eventSignal && event.target === eventSignal
? "onabort-this"
: "onabort-wrong";
}
eventSignal.addEventListener(
{ operation: "add ", toString: abortEventNameToString },
keptAbortListener,
);
eventSignal.addEventListener("abort", removedAbortListener);
eventSignal.removeEventListener(
{ operation: "remove", toString: abortEventNameToString },
removedAbortListener,
);
eventSignal.onabort = onAbortListener;
console.log("abort events before:", eventSignal.aborted);
await new Promise((resolve) => setTimeout(resolve, 5));
console.log(
"abort events after:",
abortEventNameEffects,
abortEventOrder,
eventSignal.aborted,
);
`;
}
interface TraceState {
locked: boolean;
reader: string;
readerSerial: number;
reads: number;
terminal: boolean;
}
function acquireReader(lines: string[], trace: number, state: TraceState): void {
state.readerSerial++;
state.reader = `trace${trace}Reader${state.readerSerial}`;
lines.push(`const ${state.reader} = trace${trace}Stream.getReader();`);
lines.push(
`console.log("trace ${trace} acquire ${state.readerSerial}:", trace${trace}Stream.locked);`,
);
state.locked = true;
}
function readOnce(lines: string[], trace: number, state: TraceState): void {
const part = `trace${trace}Part${state.reads}`;
lines.push(`const ${part} = await ${state.reader}.read();`);
lines.push(
`console.log("trace ${trace} read ${state.reads}:", ${part}.done, ${part}.done ? "done" : ${part}.value);`,
);
state.reads++;
}
function releaseReader(lines: string[], trace: number, state: TraceState): void {
lines.push(`${state.reader}.releaseLock();`);
lines.push(
`console.log("trace ${trace} release ${state.readerSerial}:", trace${trace}Stream.locked);`,
);
state.locked = false;
}
function oneStreamTrace(rng: XorShift32, trace: number): string {
const startChunk = rng.pick(2) === 0;
const chunks = 2 + rng.pick(3);
const pullChunks = chunks - (startChunk ? 1 : 0);
const lines: string[] = [
`let trace${trace}Pulls = 0;`,
`let trace${trace}Cancel = "none";`,
`function trace${trace}Start(controller) {`,
` console.log("trace ${trace} start:", this.marker, controller.desiredSize);`,
...(startChunk
? [
` controller.enqueue("trace-${trace}-start");`,
` console.log("trace ${trace} start enqueue:", controller.desiredSize);`,
]
: []),
`}`,
`function trace${trace}Pull(controller) {`,
` trace${trace}Pulls++;`,
` console.log("trace ${trace} pull:", this.marker, trace${trace}Pulls, controller.desiredSize);`,
` if (trace${trace}Pulls <= ${pullChunks}) {`,
` controller.enqueue("trace-${trace}-pull-" + trace${trace}Pulls);`,
` }`,
` if (trace${trace}Pulls === ${pullChunks}) controller.close();`,
`}`,
`function trace${trace}CancelFn(reason) {`,
` trace${trace}Cancel = reason;`,
` console.log("trace ${trace} cancel callback:", this.marker, reason);`,
`}`,
`const trace${trace}Source = {`,
` marker: "source-${trace}",`,
` start: trace${trace}Start,`,
` pull: trace${trace}Pull,`,
` cancel: trace${trace}CancelFn,`,
`};`,
`const trace${trace}Stream = new ReadableStream(trace${trace}Source);`,
];
const state: TraceState = {
locked: false,
reader: "",
readerSerial: 0,
reads: 0,
terminal: false,
};
acquireReader(lines, trace, state);
readOnce(lines, trace, state);
// Each residue class forces one important transition; the remaining
// steps are seeded model choices. This keeps coverage stable while still
// exploring different valid traces when the seed or count changes.
if (trace % 4 === 1) {
releaseReader(lines, trace, state);
acquireReader(lines, trace, state);
} else if (trace % 4 === 2) {
lines.push(`await ${state.reader}.cancel("reader-cancel-${trace}");`);
lines.push(`console.log("trace ${trace} reader cancel:", trace${trace}Stream.locked);`);
state.terminal = true;
} else if (trace % 4 === 3) {
releaseReader(lines, trace, state);
lines.push(`await trace${trace}Stream.cancel("stream-cancel-${trace}");`);
lines.push(`console.log("trace ${trace} stream cancel:", trace${trace}Stream.locked);`);
state.terminal = true;
}
const randomSteps = 2 + rng.pick(5);
for (let step = 0; step < randomSteps && !state.terminal; step++) {
if (!state.locked) {
if (rng.pick(3) === 0) {
lines.push(`await trace${trace}Stream.cancel("random-cancel-${trace}-${step}");`);
lines.push(
`console.log("trace ${trace} random stream cancel ${step}:", trace${trace}Stream.locked);`,
);
state.terminal = true;
} else {
acquireReader(lines, trace, state);
}
continue;
}
switch (rng.pick(4)) {
case 0:
case 1:
readOnce(lines, trace, state);
if (state.reads >= chunks) state.terminal = true;
break;
case 2:
releaseReader(lines, trace, state);
break;
default:
lines.push(`await ${state.reader}.cancel("random-reader-cancel-${trace}-${step}");`);
lines.push(
`console.log("trace ${trace} random reader cancel ${step}:", trace${trace}Stream.locked);`,
);
state.terminal = true;
break;
}
}
if (!state.locked) acquireReader(lines, trace, state);
if (!state.terminal) {
while (state.reads <= chunks) readOnce(lines, trace, state);
} else {
readOnce(lines, trace, state);
}
lines.push(`await ${state.reader}.closed;`);
lines.push(
`console.log("trace ${trace} final:", trace${trace}Pulls, trace${trace}Cancel, trace${trace}Stream.locked);`,
);
return lines.join("\n");
}
function streamTraces(seed: number, traceCount: number): string {
const rng = new XorShift32(seed);
const traces: string[] = [];
for (let trace = 0; trace < traceCount; trace++) {
traces.push(`{\n${oneStreamTrace(rng, trace)}\n}`);
}
traces.push(String.raw`
{
const streamErrorReason = { marker: "stream-error" };
const erroredStream = new ReadableStream({
start(controller) {
console.log("stream error desired size:", controller.desiredSize);
controller.error(streamErrorReason);
console.log("stream error after:", controller.desiredSize);
},
});
const erroredReader = erroredStream.getReader();
try {
await erroredReader.read();
console.log("stream error read: no-error");
} catch (error) {
console.log("stream error read:", error.marker);
}
try {
await erroredReader.closed;
console.log("stream error closed: no-error");
} catch (error) {
console.log("stream error closed:", error.marker);
}
}
try {
new ReadableStream({
start(controller) {
controller.close();
controller.close();
},
});
console.log("stream double close: no-error");
} catch (error) {
console.log("stream double close:", error.name, error.message);
}
`);
return traces.join("\n");
}
const SCENARIO_GENERATORS: Record<GeneratedScenario, (seed: number, traceCount: number) => string> =
{
"abort-events": () => abortEvents(),
"stream-traces": (seed, traceCount) => streamTraces(seed, traceCount),
"webidl-operations": () => webIdlOperations(),
};
export function generatedScenarioIds(profile: FetchCompatProfile): string[] {
return [
...new Set(
profile.operations.flatMap((operation) =>
operation.evidence.flatMap((item) =>
item.generated === undefined ? [] : [item.generated],
),
),
),
].sort();
}
export function generateFetchConformanceProgram(
profile: FetchCompatProfile,
options: { seed?: number; traceCount?: number } = {},
): string {
const seed = options.seed ?? FETCH_CONFORMANCE_SEED;
const traceCount = options.traceCount ?? 12;
const scenarios = generatedScenarioIds(profile);
const sections: string[] = [
`// Generated from node24FetchCompatProfile(). Seed: 0x${seed.toString(16)}.`,
];
for (const scenario of scenarios) {
if (!Object.hasOwn(SCENARIO_GENERATORS, scenario)) {
throw new Error(`unknown generated fetch conformance scenario: ${scenario}`);
}
sections.push(`// scenario: ${scenario}`);
sections.push(SCENARIO_GENERATORS[scenario as GeneratedScenario](seed, traceCount));
}
sections.push("export {};", "");
return sections.join("\n");
}