mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Scope-B milestone NI-1, runtime side. Programs whose IR has a `nodeIsland.*` operation link scr_node_island.c and the SCR_NODE_ISLANDS build of scr_async.c; every other program links neither, byte for byte. - scr_node_island.c dlopens the bridge of a libnode pack on the first call (RTLD_NOW | RTLD_GLOBAL), checks its bridge ABI and that it links exactly the Node version recorded at build time, starts one Node environment and runs the embedded bootstrap (scr_node_island_host.cjs). A run that never calls into Node never maps libnode. The pack directory is SCRIPTC_LIBNODE at run time, else the recorded path; a `$ORIGIN/` path resolves against the executable's directory (the beside packaging, and the hook for shipping Node with the program). - scr_node_bridge.h is the C ABI between the runtime and the bridge; the runtime includes no Node or V8 header. The bridge (packages/libnode-common/bridge/scr_node_bridge.cc) initializes Node once with no default signal handling, no debug-signal handler or inspector, no stdio reinitialization and one platform worker, creates the environment from the built-in snapshot, routes Node's process exit to the runtime's exit sequence, promotes libnode to the global scope for native addons, and runs calls in a CallbackScope with a TryCatch. build-pack.mjs assembles a pack (libnode, the bridge, manifest.json) from a Node --shared build and the matching headers. - The SCR_NODE_ISLANDS build of scr_async.c adds a loop slot through which the runtime's loop drives Node's until NI-3 hosts the runtime's queues on Node's libuv: it keeps the program alive while Node has ref'd work, sleeps on Node's backend fd up to its next timer, and runs one non-blocking turn per loop turn. - The `nodeIsland` runtime feature (rule, link features, unit table, pack matrix row and scr_async.c variant), the nodeIsland.call/callSync libCalls, the install call in main, and the build-time record of the pack (node-islands/libnode.ts: --libnode=<pack>|beside or SCRIPTC_LIBNODE, SC8005 when none is usable, Node >=24.13.1). - The zero-cost gate and the bench nm test require that no good-code executable defines a Node island symbol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
/* Two-tier invariant I3 for the runtime benchmark workloads, in CI: the
|
|
* release executable of a fully typed workload defines no dynamic-tier
|
|
* runtime symbol (DYNAMIC_TIER_PREFIXES), and every workload the zero-cost
|
|
* gate lists as dynamic (DYNAMIC_WORKLOADS) still defines one, so the list
|
|
* shrinks as leaks close. The gate (scripts/zero-cost-gate.mjs) checks the
|
|
* same contract, but only when someone runs it. The contract is the Linux
|
|
* release binary the gate measures, so other hosts and the sanitized lane
|
|
* skip the builds. */
|
|
import { execFile } from "node:child_process";
|
|
import { mkdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect, test } from "vitest";
|
|
import { compile } from "@scriptc/compiler";
|
|
import {
|
|
DYNAMIC_WORKLOADS,
|
|
definedFunctionNames,
|
|
dynamicTierSymbols,
|
|
nodeIslandSymbols,
|
|
} from "../../scripts/zero-cost-compare.mjs";
|
|
import { readWorkloads } from "../../benchmarks/runtime/workloads.mjs";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = join(import.meta.dirname, "../..");
|
|
const suite = join(repoRoot, "benchmarks/runtime");
|
|
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/bench-dynamic-tier");
|
|
const skip = process.platform !== "linux" || process.env["SCRIPTC_SAN"] === "1";
|
|
|
|
interface Workload {
|
|
name: string;
|
|
entry: string;
|
|
}
|
|
|
|
// One program per entry file, named after its first workload, as the gate
|
|
// names them.
|
|
const programs = new Map<string, string>();
|
|
for (const w of readWorkloads(suite) as Workload[])
|
|
if (![...programs.values()].includes(w.entry)) programs.set(`bench/${w.name}`, w.entry);
|
|
|
|
test("every workload the gate lists as dynamic is a bench program", () => {
|
|
for (const id of DYNAMIC_WORKLOADS) expect(programs.has(id), id).toBe(true);
|
|
});
|
|
|
|
test.skipIf(skip).each([...programs])(
|
|
"%s links the dynamic tier only if listed",
|
|
async (id, entry) => {
|
|
const outDir = join(cacheDir, id.slice("bench/".length));
|
|
mkdirSync(outDir, { recursive: true });
|
|
const result = await compile(join(suite, entry), { outPath: join(outDir, "program"), outDir });
|
|
if (!result.ok)
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
const { stdout } = await execFileAsync("nm", [result.binaryPath], {
|
|
encoding: "utf8",
|
|
maxBuffer: 1 << 28,
|
|
});
|
|
const defined = definedFunctionNames(stdout);
|
|
// No workload calls into Node.js: none links the Node island runtime.
|
|
expect(nodeIslandSymbols(defined), `${id} links Node island symbols`).toEqual([]);
|
|
const linked = dynamicTierSymbols(defined);
|
|
if (DYNAMIC_WORKLOADS.has(id))
|
|
expect(
|
|
linked.length,
|
|
`${id} links no dynamic-tier symbol: drop it from DYNAMIC_WORKLOADS`,
|
|
).toBeGreaterThan(0);
|
|
else expect(linked, `fully typed ${id} links dynamic-tier symbols`).toEqual([]);
|
|
},
|
|
);
|