Files
scriptc/packages/compiler/src/native/cli.ts
T
Chris Tate 050bbd852c Collect the frontend's cycles when the native compiler disposes it
The frontend's syntax trees and program state refer to each other, so
reference counting frees almost none of them when a build disposes the
frontend. Whether a cycle pass reclaimed them before the build ended
depended on where the collector's schedule happened to be. Passes during
the frontend found its data live and widened the growth fractions, so the
next pass waited until the heap had grown well past its size at disposal,
and the dead frontend and the emitter's garbage stayed until the build
ended.

The native compiler now runs scr_collect_cycles_phase right after it
disposes the frontend, through a host function. The runtime entry point
sweeps, then restarts the growth fractions and the death rule's backoff
from the heap the sweep leaves, as after a productive pass. Under Node
the hook is unset.
2026-10-21 09:09:19 -05:00

175 lines
7.0 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCli } from "../cli/command.js";
import type { NativeCacheWarmProfile } from "../cli/host.js";
import {
selectNativeRuntimePack,
stageNativeRuntimeSelection,
} from "../backend/runtime-pack-native.js";
import { mergeNativeProfiles } from "../backend/native-tools.js";
import { setDeclarationRoot } from "../frontend/dts-paths.js";
import { setFrontendDisposedHook } from "../frontend/dispose-hook.js";
import { FrontendServices } from "../frontend/services.js";
import { createNativeTs7Api } from "../frontend/ts7/native-api.js";
import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js";
import { setCompilerReleaseVersion } from "../library/sidecar.js";
import { NativeCompiler } from "./compiler.js";
import { openNativeCache } from "./cache.js";
import { loadNativeToolchain } from "./toolchain.js";
import { buildSanitizedRuntime } from "./sanitizer.js";
import { runCompilerTask } from "./task.js";
declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean;
declare function compilerNativeCollectCycles(): void;
async function main(): Promise<number> {
setFrontendDisposedHook(() => compilerNativeCollectCycles());
const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json";
let compiler: NativeCompiler | null = null;
let cache: ReturnType<typeof openNativeCache> = null;
const getCompiler = (): NativeCompiler => {
if (compiler !== null) return compiler;
const toolchain = loadNativeToolchain(manifestPath, process.env);
if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot);
setCompilerReleaseVersion(toolchain.compilerVersion);
cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden));
compiler = new NativeCompiler(toolchain, cache);
return compiler;
};
return runCli(process.argv.slice(2), {
version: () => loadNativeToolchain(manifestPath).compilerVersion,
sourceTargetPlatform: () => getCompiler().toolchain.target.platform,
analyze: (entry, options) => runCompilerTask(() => getCompiler().analyzeCached(entry, options)),
compile: (entry, options) => runCompilerTask(() => getCompiler().compile(entry, options)),
compileLibrary: (options) => runCompilerTask(() => getCompiler().compileLibrary(options)),
resolveProvenanceSources: async (entry) => {
const toolchain = getCompiler().toolchain;
const services = new FrontendServices((options) =>
createNativeTs7Api({ ...options, executable: toolchain.ts7Executable }),
);
try {
return await resolveProvenanceSourcesWithParser(entry, (path, source, kind) =>
services.parse(path, source, kind),
);
} finally {
services.close();
}
},
warmNativeCaches: async (options) => {
const toolchain = getCompiler().toolchain;
if (cache === null) throw new Error("the native build cache is disabled or unavailable");
if (!toolchain.target.supports.exe)
throw new Error(
`native cache warming requires an executable target; ${toolchain.target.name} supports library archives`,
);
const profiles: { profile: NativeCacheWarmProfile; elapsedMs: number }[] = [];
for (const profile of [
...new Set(options.profiles ?? (["runtime", "tls", "dynamic"] as NativeCacheWarmProfile[])),
]) {
const start = performance.now();
const stage = mkdtempSync(join(tmpdir(), "scriptc-warm-"));
try {
const pack = selectNativeRuntimePack(
toolchain.runtimePackRoot,
toolchain.target,
toolchain.compilerVersion,
{
fetch: profile === "tls",
regex: false,
copying: false,
textDecoderLegacy: false,
fileHandle: false,
zlib: false,
assert: false,
inspect: false,
dynInvoke: false,
dc: false,
dynAsync: false,
events: false,
emitter: false,
symbol: false,
bigint: false,
searchParams: false,
qs: false,
utilPromisify: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
dnsPromises: false,
dgramDyn: false,
watch: false,
foreignFfi: false,
nodeTest: false,
tls: false,
cipher: false,
ecdh: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
},
options.optimization ?? "release",
);
if (options.sanitize) buildSanitizedRuntime(toolchain, pack, stage, "executable", cache);
else stageNativeRuntimeSelection(pack, stage);
} finally {
rmSync(stage, { recursive: true, force: true });
}
profiles.push({ profile, elapsedMs: performance.now() - start });
}
return { cacheRoot: cache.root, profiles };
},
mergeProfiles: async (outputPath, inputPaths, sparse) => {
const toolchain = getCompiler().toolchain;
mergeNativeProfiles({
executable: toolchain.helperExecutable,
packageRoot: toolchain.helperPackageRoot,
compilerVersion: toolchain.compilerVersion,
target: toolchain.target,
helper: toolchain.helper,
outputPath,
inputPaths,
sparse,
});
},
run: async (binary) => {
const toolchain = getCompiler().toolchain;
let executable = binary;
const args: string[] = [];
if (toolchain.target.platform === "wasi") {
if (toolchain.wasiNodeRunner === undefined)
throw new Error("the WASI runner is missing from this installation");
executable = "node";
args.push("--no-warnings", toolchain.wasiNodeRunner, binary);
}
const result = spawnSync(executable, args, { stdio: "inherit" });
if (result.error) {
if (toolchain.target.platform === "wasi") {
throw new Error(
`could not start the Node.js WASI host: ${result.error.message}; running WASI modules requires Node.js 24 or newer on PATH; use scriptc build to compile without Node`,
);
}
throw new Error(result.error.message);
}
if (result.signal) {
process.stderr.write(`scriptc: program killed by ${result.signal}\n`);
return 1;
}
return result.status ?? 0;
},
});
}
try {
process.exitCode = await main();
} catch (error) {
process.stderr.write(`scriptc: ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}