Files
scriptc/packages/cli/src/bootstrap.ts
T
Malte UblandClaude Opus 5.5 4a8a6567ca Add the shape overlay: typed JavaScript from analysis-proven facts (opt-in)
The shape overlay (DESIGN.md, approved route) rewrites the JavaScript of
opted-in npm packages with JSDoc types the shape analysis proves: record
typedefs for non-escaping literal sites (optional fields for spread
sources), class field, parameter, return and local types, receiver casts,
and branches the analysis proved unreachable replaced by checks. The
lowering trusts only annotations carrying the overlay's marker; package
JSDoc stays a hint (its type tags are neutralized in overlaid files).

- Inferred-fact checks: every checked conversion lowered in an overlaid
  file records its site, and a failure throws a distinct internal error
  ("scriptc internal error: an inferred type was contradicted at <file:line>
  … This is a bug in scriptc's shape analysis, not in the program's data")
  naming the opt-outs; SCRIPTC_SHAPE_CHECK_LOG makes checks log instead
  (validation mode).
- Opt-outs: `--no-shape-overlay`, and `"scriptc": {"packages": {"<name>":
  {"shapes": false}}}` per package.
- Build report: SC6103 counts the declarations typed, the checks inserted
  and the branches replaced.
- `x instanceof Promise` on plain data folds to false.

The overlay runs only with SCRIPTC_SHAPE_OVERLAY=1 while it is new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:24:51 -07:00

256 lines
9.5 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { readFile, rm } from "node:fs/promises";
import { enableCompileCache } from "node:module";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { CLI_OPTIONS, USAGE } from "./usage.js";
// Node 24 can persist V8's compiled module bytecode. scriptc's CLI imports
// the compiler and its lowering/backend graph before handling any command, so
// enabling this in the tiny bootstrap avoids reparsing that graph on every
// edit/build invocation.
try {
enableCompileCache();
} catch {
// Bytecode caching is an optimization boundary. A read-only temp directory
// must never prevent the compiler from running.
}
function packageVersion(): string {
const here = dirname(fileURLToPath(import.meta.url));
try {
return (
(JSON.parse(requireText(join(here, "..", "package.json"))) as { version?: string }).version ??
"unknown"
);
} catch {
return "unknown";
}
}
function requireText(path: string): string {
// This tiny synchronous read keeps --version free of the compiler graph and
// preserves the package manifest as the one release-version authority.
const { readFileSync } = process.getBuiltinModule("node:fs") as typeof import("node:fs");
return readFileSync(path, "utf8");
}
async function tryFastPath(): Promise<number | null> {
let parsed: ReturnType<
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
>;
try {
parsed = parseArgs({ options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
} catch {
return null; // main owns exact user-error wording
}
const { values, positionals } = parsed;
if (values.version) {
process.stdout.write(`${packageVersion()}\n`);
return 0;
}
if (values.help || positionals.length === 0) {
process.stdout.write(USAGE);
return values.help ? 0 : 1;
}
const [command, inputArg] = positionals;
if (command === "coverage") return coverageFastPath(values, inputArg, positionals.length);
if (
(command !== "build" && command !== "run") ||
inputArg === undefined ||
(values.emit !== undefined && values.emit !== "exe") ||
values.print !== undefined ||
values["emit-ir"] ||
values.lib ||
values["provenance-sources"] ||
// The full compiler keys these builds by the profile's bytes.
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
values.sparse ||
// The full compiler resolves `native` against the build host.
values["target-cpu"] !== undefined ||
(values["external-types"] ?? []).length > 0
)
return null;
const backend = values.backend;
if (backend !== undefined && backend !== "llvm") return null;
const optimization = values.optimization;
if (
optimization !== undefined &&
optimization !== "release" &&
optimization !== "dev" &&
optimization !== "speed"
)
return null;
const npmRaw = (values["npm-static"] ?? [])
.flatMap((value) => value.split(","))
.map((value) => value.trim())
.filter((value) => value !== "");
let npmStatic: string[] | "auto" | null = null;
if (npmRaw.includes("auto")) {
if (npmRaw.length !== 1) return null;
npmStatic = "auto";
} else if (npmRaw.length > 0) {
npmStatic = npmRaw;
}
let startup: typeof import("@scriptc/compiler/startup-cache");
let buildPlatform: string;
try {
startup = await import("@scriptc/compiler/startup-cache");
buildPlatform = startup.configuredTargetPlatform();
} catch {
return null;
}
if (command === "run" && buildPlatform === "wasi") return null;
const input = resolve(inputArg);
const outDir = values.out ? dirname(resolve(values.out)) : join(dirname(input), ".scriptc");
const stem = basename(input).replace(/\.(ts|mts|cts|js|mjs|cjs|c|ll)$/, "");
const defaultName =
buildPlatform === "win32" ? `${stem}.exe` : buildPlatform === "wasi" ? `${stem}.wasm` : stem;
const outPath = values.out ? resolve(values.out) : join(outDir, defaultName);
const ffiPath = values.ffi === undefined ? null : resolve(values.ffi);
const ffiBytes = ffiPath === null ? null : await readFile(ffiPath).catch(() => null);
if (ffiPath !== null && ffiBytes === null) return null;
const root = await startup.prepareBuildCacheRoot(startup.resolveBuildCacheRoot());
// This must exactly mirror the ordinary LLVM executable's route in the
// full compiler. Otherwise a valid helper/runtime-pack cache entry has a
// different target/compiler identity and bootstrap must unnecessarily load
// the whole compiler graph to rediscover it.
const helperRuntimePackTarget = !values.sanitize ? startup.precompiledRuntimePackTarget() : null;
const helperObjectRoute = helperRuntimePackTarget !== null;
// The full compiler reports a checker it cannot find.
let checker: string | undefined;
try {
checker = startup.checkerCacheIdentity();
} catch {
return null;
}
const hit = await startup.readRoutedExecutableCache(root, {
entryPath: input,
outDir,
outPath,
emitIr: values["emit-ir"],
sanitize: values.sanitize,
backend: "llvm",
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
...(values.strip ? { strip: true as const } : {}),
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
...(checker === undefined ? {} : { checker }),
npmStatic,
// Mirrors the full compiler's key: shape inference's overlay is opt-in
// (SCRIPTC_SHAPE_OVERLAY=1) and --no-shape-overlay turns it off.
...(process.env["SCRIPTC_SHAPE_OVERLAY"] === "1" && values["shape-overlay"] !== false
? { shapeOverlay: true as const }
: {}),
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
helperObjectRoute ? "runtime-pack" : "driver-tu"
}`,
compiler: [
helperObjectRoute
? startup.resolvePlatformLinker(process.env, helperRuntimePackTarget.defaultLinker)
: (process.env["SCRIPTC_CC"] ?? "clang"),
],
nativeEnvironment: () =>
helperObjectRoute
? startup.executableLinkerEnvironmentFingerprint(
process.env,
helperRuntimePackTarget.defaultLinker,
)
: startup.executableNativeEnvironmentFingerprint(),
nodeVersion: process.version,
});
if (hit === null) return null;
if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true });
if (command === "build") {
// A cached build repeats its performance warning (SC6101).
const performance = hit.performance ?? [];
if (performance.length > 0 && values["dynamic-tier-warning"] !== false)
process.stderr.write(startup.renderPerformanceWarnings(performance) + "\n");
process.stdout.write(`${outPath}\n`);
return 0;
}
return new Promise<number>((resolveExit) => {
const child = spawn(outPath, [], { stdio: "inherit" });
child.on("exit", (code, signal) => {
if (signal) {
process.stderr.write(`scriptc: program killed by ${signal}\n`);
resolveExit(1);
} else {
resolveExit(code ?? 0);
}
});
});
}
/** An unchanged program's coverage verdict replays from the verdict cache
* without loading the compiler graph. Anything else (a miss, a comment-only
* edit, options the replay does not cover) defers to the full CLI. */
async function coverageFastPath(
values: ReturnType<
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
>["values"],
inputArg: string | undefined,
positionalCount: number,
): Promise<number | null> {
const print = values.print;
const failOn = values["fail-on"];
if (
inputArg === undefined ||
positionalCount !== 2 ||
(print !== undefined && print !== "diagnostics") ||
(failOn !== undefined && failOn !== "blockers" && failOn !== "divergences") ||
values.emit !== undefined ||
values.strip ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values.lib ||
values.profile !== undefined ||
values.ffi !== undefined ||
values.backend !== undefined ||
values.optimization !== undefined ||
values["windows-subsystem"] !== undefined ||
values["provenance-sources"] ||
values["emit-ir"] ||
(values["npm-static"] ?? []).length > 0 ||
values["shape-overlay"] === false ||
(values["external-types"] ?? []).length > 0
)
return null;
let startup: typeof import("@scriptc/compiler/startup-cache");
try {
startup = await import("@scriptc/compiler/startup-cache");
} catch {
return null;
}
const hit = await startup.readCoverageVerdict(resolve(inputArg), {}).catch(() => null);
if (hit === null) return null;
const { coverage, sourceTexts } = hit;
if (values["dynamic-tier-warning"] === false) delete coverage.performance;
if (print === "diagnostics") {
const envelope = startup.coverageEnvelope(coverage, {
compilerVersion: packageVersion(),
sourceTexts,
...(failOn === undefined ? {} : { failOn }),
});
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
} else {
const color = process.stdout.isTTY ?? false;
process.stdout.write(
startup.renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
);
}
return startup.coveragePasses(coverage, failOn) ? 0 : 1;
}
const fastExit = await tryFastPath();
if (fastExit === null) {
await import("./main.js");
} else {
process.exitCode = fastExit;
}