mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Compiler caches keyed by programs and lowering passes move onto the objects that own their lifetime, and the field-creation pass recognizes its own writes by their receiver temporaries. A source test keeps weak collections out of the compiler. Records and arrays that reach an untyped slot through a union now keep their identity, and empty object literals are keyed by their native object, so both work as WeakMap and WeakSet keys instead of failing at runtime.
570 lines
25 KiB
TypeScript
570 lines
25 KiB
TypeScript
import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
|
|
import { execFile, spawnSync } from "node:child_process";
|
|
import {
|
|
chmodSync,
|
|
cpSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
renameSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, join, resolve } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { pathToFileURL } from "node:url";
|
|
import { WASI } from "node:wasi";
|
|
import { expect, test } from "vitest";
|
|
import type { NativeToolchainManifest } from "../../packages/compiler/src/native/toolchain.js";
|
|
import { RUNTIME_ABI_MARKER } from "../../packages/compiler/src/backend/runtime-abi.js";
|
|
import {
|
|
nativeBootstrapPlan,
|
|
runNativeBootstrapChecks,
|
|
} from "../../scripts/ci-native-bootstrap.mjs";
|
|
import { bootstrapStep } from "./self-hosting-timing.js";
|
|
|
|
const root = join(import.meta.dirname, "../..");
|
|
const exec = promisify(execFile);
|
|
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
|
const phase = process.env["SCRIPTC_BOOTSTRAP_PHASE"] ?? "all";
|
|
const plan = nativeBootstrapPlan({ phase, sanitize });
|
|
|
|
function comparableStderr(text: string): string {
|
|
return sanitize
|
|
? text.replace(
|
|
/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm,
|
|
"",
|
|
)
|
|
: text;
|
|
}
|
|
|
|
function absoluteCommand(command: string): string {
|
|
if (command.includes("/") || command.includes("\\")) return resolve(command);
|
|
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd"] : [""];
|
|
for (const directory of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
for (const extension of extensions) {
|
|
const path = join(directory, command + extension);
|
|
if (existsSync(path)) return resolve(path);
|
|
}
|
|
}
|
|
throw new Error(`native tool is not on PATH: ${command}`);
|
|
}
|
|
|
|
test(
|
|
`the production CLI passes ${phase} bootstrap contracts with Node unavailable`,
|
|
async () => {
|
|
const sharedSeed = process.env["SCRIPTC_BOOTSTRAP_SEED_DIRECTORY"];
|
|
const directory =
|
|
sharedSeed ??
|
|
mkdtempSync(
|
|
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-bootstrap-"),
|
|
);
|
|
if (sharedSeed) verifyBootstrapSeed(sharedSeed, process.env["GITHUB_SHA"], sanitize);
|
|
const executable = (name: string) =>
|
|
join(directory, name + (process.platform === "win32" ? ".exe" : ""));
|
|
const options = {
|
|
cwd: root,
|
|
timeout: sanitize ? 5_400_000 : 1_800_000,
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
};
|
|
try {
|
|
const distribution = join(directory, "distribution");
|
|
if (!sharedSeed)
|
|
await bootstrapStep("build production CLI seed", () =>
|
|
exec(
|
|
process.execPath,
|
|
[
|
|
"--max-old-space-size=8192",
|
|
"--import",
|
|
"tsx",
|
|
join(root, "scripts/build-native-cli.mts"),
|
|
distribution,
|
|
],
|
|
{
|
|
// The plain lane retains release optimization. Instrumenting a dev
|
|
// seed avoids optimizing the huge compiler module before ASan can
|
|
// exercise its complete traversal, serialization and LLVM emission.
|
|
...options,
|
|
env: {
|
|
...process.env,
|
|
SCRIPTC_SAN: sanitize ? "1" : "",
|
|
SCRIPTC_NATIVE_EMIT_IR: "1",
|
|
SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release",
|
|
},
|
|
},
|
|
),
|
|
);
|
|
// CI reuses this already-built seed for npm and older-libc installation
|
|
// smoke tests, avoiding another full compiler build on the critical path.
|
|
const packageDirectory = process.env["SCRIPTC_BOOTSTRAP_PACKAGE_DIR"];
|
|
if (plan.packageChecks && packageDirectory !== undefined) {
|
|
if (process.platform !== "linux" || process.arch !== "x64")
|
|
throw new Error("bootstrap package export requires the Linux x64 GNU CI host");
|
|
mkdirSync(packageDirectory, { recursive: true });
|
|
cpSync(
|
|
join(root, "packages/cli-linux-x64-gnu/package.json"),
|
|
join(packageDirectory, "package.json"),
|
|
);
|
|
cpSync(distribution, join(packageDirectory, "dist"), { recursive: true });
|
|
writeFileSync(packageDirectory + ".ready", "ready\n");
|
|
}
|
|
// All compiler assets must survive moving the complete distribution.
|
|
const relocated = join(directory, "relocated");
|
|
renameSync(distribution, relocated);
|
|
const seed = join(relocated, "bin", "scriptc" + (process.platform === "win32" ? ".exe" : ""));
|
|
const manifestPath = seed + ".json";
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NativeToolchainManifest;
|
|
manifest.linker = absoluteCommand(manifest.linker);
|
|
if (process.platform === "linux") {
|
|
// Clang delegates linking to a separate executable. Resolve it before
|
|
// removing PATH so the complete native toolchain remains available.
|
|
const linker = await exec(
|
|
manifest.linker,
|
|
[...manifest.linker_args, "--print-prog-name=ld"],
|
|
options,
|
|
);
|
|
manifest.linker_args.push("--ld-path=" + absoluteCommand(linker.stdout.trim()));
|
|
}
|
|
if (process.platform === "darwin") manifest.dsymutil = absoluteCommand(manifest.dsymutil);
|
|
if (process.platform === "darwin") manifest.relocatable_linker = absoluteCommand("ld");
|
|
manifest.archiver = absoluteCommand(process.platform === "win32" ? "zig" : "ar");
|
|
manifest.archiver_args = process.platform === "win32" ? ["ar"] : [];
|
|
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
let sanitizerCompiler: string | undefined;
|
|
if (sanitize) {
|
|
sanitizerCompiler = absoluteCommand(process.env["SCRIPTC_CC"] ?? "clang");
|
|
if (process.platform === "linux") {
|
|
const linker = await exec(sanitizerCompiler, ["--print-prog-name=ld"], options);
|
|
const quote = (value: string) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
const wrapper = join(directory, "sanitizer-clang");
|
|
// Preserve the empty PATH while making Clang's child linker explicit.
|
|
writeFileSync(
|
|
wrapper,
|
|
`#!/bin/sh\nexec ${quote(sanitizerCompiler)} ${quote("--ld-path=" + absoluteCommand(linker.stdout.trim()))} "$@"\n`,
|
|
);
|
|
chmodSync(wrapper, 0o755);
|
|
sanitizerCompiler = wrapper;
|
|
}
|
|
}
|
|
const nativeOptions = {
|
|
...options,
|
|
env: {
|
|
...process.env,
|
|
PATH: "",
|
|
SCRIPTC_TOOLCHAIN: manifestPath,
|
|
SCRIPTC_CACHE_DIR: join(directory, "cache"),
|
|
...(sanitizerCompiler === undefined ? {} : { SCRIPTC_CC: sanitizerCompiler }),
|
|
},
|
|
};
|
|
const invoke = async (
|
|
compiler: string,
|
|
args: string[],
|
|
expectedStderr = "",
|
|
env: Record<string, string> = {},
|
|
) => {
|
|
const result = await exec(compiler, args, {
|
|
...nativeOptions,
|
|
env: { ...nativeOptions.env, ...env },
|
|
}).catch((error: unknown) => {
|
|
const failure = error as Error & {
|
|
code?: string | number;
|
|
signal?: string;
|
|
stdout?: string;
|
|
stderr?: string;
|
|
};
|
|
throw new Error(
|
|
`${failure.message}\ncode=${failure.code} signal=${failure.signal}\n${failure.stderr ?? ""}\n${failure.stdout ?? ""}`,
|
|
{ cause: error },
|
|
);
|
|
});
|
|
expect(comparableStderr(result.stderr)).toBe(expectedStderr);
|
|
return result.stdout;
|
|
};
|
|
const entry = join(root, "packages/compiler/src/native/cli.ts");
|
|
const ffi = join(directory, ".scriptc/distribution-seed/compiler.ffi.json");
|
|
const rebuilt = executable("scriptc-rebuilt");
|
|
const checkProgram = async (compiler: string, source: string, extra: string[] = []) => {
|
|
// This basename formerly collided with the driver's temporary object.
|
|
const probeDirectory = join(
|
|
directory,
|
|
compiler === seed ? "seed-probes" : "rebuilt-probes",
|
|
);
|
|
mkdirSync(probeDirectory, { recursive: true });
|
|
const output = join(
|
|
probeDirectory,
|
|
"program.o" + (process.platform === "win32" ? ".exe" : ""),
|
|
);
|
|
const built = await invoke(compiler, [
|
|
"build",
|
|
source,
|
|
"-o",
|
|
output,
|
|
"--optimization=dev",
|
|
"--strip",
|
|
...extra,
|
|
]);
|
|
expect(built.trim()).toBe(output);
|
|
const oracle = spawnSync(process.execPath, [source], options);
|
|
const actual = spawnSync(output, [], nativeOptions);
|
|
for (const result of [oracle, actual]) {
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.signal).toBeNull();
|
|
expect(result.status, result.stderr.toString()).toBe(0);
|
|
}
|
|
expect(actual.stdout).toEqual(oracle.stdout);
|
|
expect(actual.stderr).toEqual(oracle.stderr);
|
|
};
|
|
const sample = join(root, "tests/corpus/class-array-optional-return.ts");
|
|
const unionSample = join(root, "tests/corpus/union-nested-layout-discriminant.ts");
|
|
const receiverSample = join(root, "tests/corpus/llvm-read-receiver-lifetime.ts");
|
|
// CI assigns command probes and self-rebuilds to separate runners. A
|
|
// direct invocation retains both phases and drains them before cleanup.
|
|
const seedChecks = async () => {
|
|
// All command, library, and dynamic probes run inside the instrumented
|
|
// compiler in the sanitizer lane, including their failure paths.
|
|
const probe = seed;
|
|
expect(await invoke(probe, ["--help"])).toContain("scriptc build");
|
|
expect((await invoke(probe, ["--version"])).trim()).toBe(manifest.compiler_version);
|
|
const linked = join(directory, "package link");
|
|
symlinkSync(relocated, linked, process.platform === "win32" ? "junction" : "dir");
|
|
const linkedSeed = join(
|
|
linked,
|
|
"bin",
|
|
"scriptc" + (process.platform === "win32" ? ".exe" : ""),
|
|
);
|
|
const hello = join(root, "tests/corpus/001-hello.ts");
|
|
const oracle = await exec(process.execPath, [hello], options);
|
|
const throughLink = await exec(
|
|
linkedSeed,
|
|
["run", hello, "-o", executable("linked-hello"), "--optimization=dev", "--strip"],
|
|
{
|
|
...nativeOptions,
|
|
env: { ...nativeOptions.env, SCRIPTC_TOOLCHAIN: undefined },
|
|
},
|
|
);
|
|
expect(throughLink.stdout).toBe(oracle.stdout);
|
|
expect(comparableStderr(throughLink.stderr)).toBe(oracle.stderr);
|
|
await checkProgram(probe, sample);
|
|
await checkProgram(probe, unionSample);
|
|
await checkProgram(probe, receiverSample);
|
|
await checkProgram(probe, join(root, "tests/corpus/closure-nullable-union-return.ts"));
|
|
await checkProgram(probe, join(root, "tests/corpus/record-optional-json-presence.ts"));
|
|
await checkProgram(probe, join(root, "tests/corpus/1010-json-stringify-space.ts"));
|
|
await checkProgram(probe, join(root, "tests/corpus/fs-write-string-bytes-union.ts"));
|
|
// Exact import-cycle initialization and weak keys from unions and empty objects.
|
|
await checkProgram(probe, join(root, "tests/corpus/4592-cycle-const-from-import/main.ts"));
|
|
await checkProgram(probe, join(root, "tests/corpus/4591-weak-keys-union-records.ts"));
|
|
await checkProgram(probe, join(root, "tests/corpus/4593-weak-keys-empty-objects.ts"));
|
|
|
|
const fetchOptions = join(directory, "fetch-options.ts");
|
|
writeFileSync(
|
|
fetchOptions,
|
|
'async function probe() { const response = await fetch("https://example.invalid", { headers: { accept: "application/json" } }); console.log(response.status); } if (process.env["RUN_NATIVE_FETCH_PROBE"] === "1") await probe();\n',
|
|
);
|
|
await checkProgram(probe, fetchOptions);
|
|
expect(await invoke(probe, ["coverage", fetchOptions])).toContain("(100%)");
|
|
|
|
const dynamic = join(directory, "dynamic.ts");
|
|
writeFileSync(
|
|
dynamic,
|
|
"const value: any = { answer: 42 }; console.log(`answer:${value.answer}`);\n",
|
|
);
|
|
await checkProgram(probe, dynamic, ["--dynamic"]);
|
|
const comptime = join(directory, "comptime.ts");
|
|
writeFileSync(
|
|
comptime,
|
|
"const answer = comptime(() => [1, 2, 3].reduce((sum, value) => sum + value, 0) * 7); console.log(answer);\n",
|
|
);
|
|
expect(
|
|
await invoke(probe, ["run", comptime, "-o", executable("comptime"), "--strip"]),
|
|
).toBe("42\n");
|
|
|
|
const object = join(directory, "program.obj");
|
|
const linkInfo = JSON.parse(
|
|
await invoke(probe, ["build", sample, "-o", object, "--print=native-link-info"]),
|
|
);
|
|
expect(linkInfo.program.object).toBe(object);
|
|
expect(readFileSync(object).length).toBeGreaterThan(0);
|
|
|
|
const profile = join(root, "tests/library-mode/contract-attest/profile.json");
|
|
const archive = join(directory, "contract.a");
|
|
const expectedArchive = join(directory, "contract-node.a");
|
|
await exec(
|
|
process.execPath,
|
|
[
|
|
join(root, "packages/cli/dist/main.js"),
|
|
"build",
|
|
"--lib",
|
|
"--profile",
|
|
profile,
|
|
"-o",
|
|
expectedArchive,
|
|
],
|
|
options,
|
|
);
|
|
await invoke(probe, ["build", "--lib", "--profile", profile, "-o", archive]);
|
|
expect(JSON.parse(readFileSync(archive + ".contract.json", "utf8"))).toEqual(
|
|
JSON.parse(readFileSync(expectedArchive + ".contract.json", "utf8")),
|
|
);
|
|
|
|
// The unmodified package exercises deep validator traversals. Compile it
|
|
// through the installed CLI, including its async command boundary.
|
|
const threeProfile = join(root, "tests/library-mode/wasm/three.json");
|
|
await invoke(probe, [
|
|
"build",
|
|
"--lib",
|
|
"--profile",
|
|
threeProfile,
|
|
"-o",
|
|
join(directory, "three.a"),
|
|
]);
|
|
if (
|
|
process.platform !== "win32" &&
|
|
spawnSync("zig", ["version"]).status === 0 &&
|
|
existsSync(join(root, "packages/runtime-wasm32-wasi/runtime-pack.json"))
|
|
) {
|
|
const linker = join(directory, "zigcc");
|
|
writeFileSync(
|
|
linker,
|
|
`#!/bin/sh\nexec '${absoluteCommand("zig").replaceAll("'", "'\\''")}' cc "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const output = join(directory, "three.wasm");
|
|
// Install the cross-target pack after relocating the native compiler.
|
|
// Its resolver must discover project dependencies without Node on PATH.
|
|
const scope = join(directory, "node_modules/@scriptc");
|
|
mkdirSync(scope, { recursive: true });
|
|
symlinkSync(
|
|
join(root, "packages/runtime-wasm32-wasi"),
|
|
join(scope, "runtime-wasm32-wasi"),
|
|
"dir",
|
|
);
|
|
const built = await exec(
|
|
probe,
|
|
["build", "--lib", "--profile", threeProfile, "-o", output],
|
|
{
|
|
...nativeOptions,
|
|
cwd: directory,
|
|
env: {
|
|
...nativeOptions.env,
|
|
SCRIPTC_TARGET: "wasm32-wasi",
|
|
SCRIPTC_LINKER: linker,
|
|
SCRIPTC_RUNTIME_PACK: undefined,
|
|
SCRIPTC_NO_CACHE: "1",
|
|
},
|
|
},
|
|
);
|
|
expect(built.stdout.trim()).toBe(output);
|
|
expect(comparableStderr(built.stderr)).toBe("");
|
|
const vertices: number[][] = [];
|
|
const surfaces: number[][] = [];
|
|
const resources: number[][] = [];
|
|
const wasi = new WASI({ version: "preview1" });
|
|
const instance = new WebAssembly.Instance(new WebAssembly.Module(readFileSync(output)), {
|
|
...wasi.getImportObject(),
|
|
scriptc: {
|
|
vertex: (...args: number[]) =>
|
|
vertices.push(args.map((value) => Number(value.toFixed(9)))),
|
|
surface: (...args: number[]) =>
|
|
surfaces.push(args.map((value) => Number(value.toFixed(9)))),
|
|
resource: (...args: number[]) => resources.push(args),
|
|
panic: () => {
|
|
throw new Error("unexpected Wasm panic");
|
|
},
|
|
},
|
|
});
|
|
wasi.initialize(instance);
|
|
const api = instance.exports as Record<string, (...args: number[]) => number>;
|
|
api.app_init!();
|
|
for (const time of [0, 123, 2000]) expect(api.app_frame!(time, 1.5)).toBe(24);
|
|
api.app_dispose!();
|
|
const oracle = await exec(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
`
|
|
const vertices = [], surfaces = [], resources = [];
|
|
globalThis.vertex = (...args) => vertices.push(args.map(value => Number(value.toFixed(9))));
|
|
globalThis.surface = (...args) => surfaces.push(args.map(value => Number(value.toFixed(9))));
|
|
globalThis.resource = (...args) => resources.push(args);
|
|
const { frame, dispose } = await import(${JSON.stringify(join(root, "tests/library-mode/wasm/three.mjs"))});
|
|
for (const time of [0, 123, 2000]) frame(time, 1.5);
|
|
dispose();
|
|
console.log(JSON.stringify({ vertices, surfaces, resources }));
|
|
`,
|
|
],
|
|
options,
|
|
);
|
|
expect({ vertices, surfaces, resources }).toEqual(JSON.parse(oracle.stdout));
|
|
expect(oracle.stderr).toBe("");
|
|
api.app_collect!();
|
|
}
|
|
|
|
const badSource = join(directory, "bad.ts");
|
|
writeFileSync(badSource, 'const value: number = "wrong"; console.log(value);\n');
|
|
const retained = executable("retained");
|
|
writeFileSync(retained, "existing output");
|
|
const failed = spawnSync(probe, ["build", badSource, "-o", retained], nativeOptions);
|
|
expect(failed.status).toBe(1);
|
|
expect(failed.stderr.toString()).toContain("not assignable");
|
|
expect(readFileSync(retained, "utf8")).toBe("existing output");
|
|
};
|
|
// The production chain owns the complete frontend and emitter proof:
|
|
// compare the Node seed's IR and LLVM with the native self-rebuild, then
|
|
// execute the next compiler generation. Large structural checks run in a
|
|
// roomy child so Vitest's worker remains responsive.
|
|
const seedDirectory = join(directory, ".scriptc/distribution-seed");
|
|
const seedIr = join(seedDirectory, "cli.ir.json");
|
|
const compareArtifacts = async ({ ir, llvm }: { ir: boolean; llvm: boolean }) => {
|
|
const comparison = await bootstrapStep(
|
|
`compare Node and native compiler ${[ir ? "IR" : "", llvm ? "LLVM" : ""].filter(Boolean).join(" and ")}`,
|
|
() =>
|
|
exec(
|
|
process.execPath,
|
|
[
|
|
"--max-old-space-size=8192",
|
|
"--import",
|
|
"tsx",
|
|
"--input-type=module",
|
|
"--eval",
|
|
`
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { isDeepStrictEqual } from 'node:util';
|
|
import { deserializeModule, validateModule } from ${JSON.stringify(pathToFileURL(join(root, "packages/compiler/src/index.ts")).href)};
|
|
if (${ir}) {
|
|
const expected = deserializeModule(readFileSync(process.argv[1], 'utf8'));
|
|
const actual = deserializeModule(readFileSync(process.argv[2], 'utf8'));
|
|
assert.ok(actual.functions.length > 1000);
|
|
assert.deepEqual(validateModule(actual), []);
|
|
assert.ok(isDeepStrictEqual(actual, expected), 'native self-lowering must match the Node seed');
|
|
}
|
|
if (${llvm}) {
|
|
const seedLlvm = readFileSync(process.argv[3], 'utf8');
|
|
let nativeLlvm = readFileSync(process.argv[4], 'utf8');
|
|
if (${sanitize}) {
|
|
// The Node sanitizer builds runtime sources directly; the native
|
|
// executable path also emits the runtime ABI check. Pin that
|
|
// difference before comparing the rest of the complete module.
|
|
const declaration = 'declare void @' + ${JSON.stringify(RUNTIME_ABI_MARKER)} + '()\\n';
|
|
const call = ' call void @' + ${JSON.stringify(RUNTIME_ABI_MARKER)} + '()\\n';
|
|
for (const line of [declaration, call]) {
|
|
assert.equal(seedLlvm.split(line).length, 1);
|
|
assert.equal(nativeLlvm.split(line).length, 2);
|
|
nativeLlvm = nativeLlvm.replace(line, '');
|
|
}
|
|
}
|
|
assert.ok(seedLlvm === nativeLlvm, 'native LLVM emission must match the Node seed');
|
|
}
|
|
`,
|
|
seedIr,
|
|
join(directory, "cli.ir.json"),
|
|
join(seedDirectory, "cli.ll"),
|
|
join(directory, "cli.ll"),
|
|
],
|
|
options,
|
|
),
|
|
);
|
|
expect(comparison.stdout).toBe("");
|
|
expect(comparison.stderr).toBe("");
|
|
};
|
|
const probeRebuilt = async () => {
|
|
await checkProgram(rebuilt, join(root, "tests/corpus/nullish-long-chain.ts"));
|
|
await checkProgram(rebuilt, sample);
|
|
await checkProgram(rebuilt, unionSample);
|
|
await checkProgram(rebuilt, receiverSample);
|
|
await checkProgram(rebuilt, join(root, "tests/corpus/1010-json-stringify-space.ts"));
|
|
};
|
|
const settle = async (checks: Promise<void>[]) => {
|
|
for (const check of await Promise.allSettled(checks))
|
|
if (check.status === "rejected") throw check.reason;
|
|
};
|
|
// --strip keeps debug metadata out of the IR/LLVM equality comparison.
|
|
const rebuildChecks = async () => {
|
|
// The plain lane optimizes the next generation for its fixed-point check.
|
|
const self = await bootstrapStep("production CLI rebuilds itself", () =>
|
|
invoke(
|
|
seed,
|
|
["build", entry, "-o", rebuilt, "--strip", "--keep-llvm", "--emit-ir", "--ffi", ffi],
|
|
"scriptc: warning: --emit-ir is deprecated; use --emit=ir for IR as the primary output\n",
|
|
),
|
|
);
|
|
expect(self.trim()).toBe(rebuilt);
|
|
const fixedPoint = async () => {
|
|
// The plain lane owns the third-generation fixed point. The sanitizer
|
|
// lane runs the complete frontend and emitter under ASan in its two
|
|
// rebuild halves, compares both artifacts, and executes the new
|
|
// compiler.
|
|
const seedLlvm = join(directory, "cli.ll");
|
|
const rebuiltLlvm = join(directory, "rebuilt.ll");
|
|
await bootstrapStep("rebuilt compiler emits itself", () =>
|
|
invoke(rebuilt, ["build", entry, "--emit=llvm", "-o", rebuiltLlvm, "--ffi", ffi]),
|
|
);
|
|
// Executable output adds a runtime ABI check; textual LLVM emission
|
|
// omits it. Compare all other output without changing either mode.
|
|
const withoutAbiCheck = (path: string): string =>
|
|
readFileSync(path, "utf8")
|
|
.replace(/^declare void @scr_runtime_abi_v\d+\(\)\n/m, "")
|
|
.replace(/^ call void @scr_runtime_abi_v\d+\(\)\n/m, "");
|
|
expect(
|
|
withoutAbiCheck(seedLlvm) === withoutAbiCheck(rebuiltLlvm),
|
|
"native compiler generations must emit identical LLVM",
|
|
).toBe(true);
|
|
};
|
|
await settle([compareArtifacts({ ir: true, llvm: true }), probeRebuilt(), fixedPoint()]);
|
|
};
|
|
// The sanitizer lane splits the instrumented self-rebuild at the IR
|
|
// boundary. Its frontend half lowers the compiler inside the instrumented
|
|
// seed and compares that IR with the seed's; lowering does not depend on
|
|
// the output's sanitizer or optimization options. Its emitter half runs
|
|
// the instrumented LLVM emitter on the seed's IR (equal to the native IR
|
|
// by the frontend half's comparison), compares the LLVM, then links and
|
|
// executes an instrumented development compiler.
|
|
const rebuildFrontend = async () => {
|
|
const lowered = join(directory, "cli.ir.json");
|
|
const output = await bootstrapStep("sanitized native frontend lowers the compiler", () =>
|
|
invoke(seed, ["build", entry, "--emit=ir", "-o", lowered, "--ffi", ffi]),
|
|
);
|
|
expect(output.trim()).toBe(lowered);
|
|
await compareArtifacts({ ir: true, llvm: false });
|
|
};
|
|
const rebuildEmit = async () => {
|
|
const self = await bootstrapStep("sanitized native emitter rebuilds the compiler", () =>
|
|
invoke(
|
|
seed,
|
|
[
|
|
"build",
|
|
entry,
|
|
"-o",
|
|
rebuilt,
|
|
"--strip",
|
|
"--keep-llvm",
|
|
"--ffi",
|
|
ffi,
|
|
"--sanitize",
|
|
"--optimization=dev",
|
|
],
|
|
"",
|
|
{ SCRIPTC_BOOTSTRAP_IR_INPUT: seedIr },
|
|
),
|
|
);
|
|
expect(self.trim()).toBe(rebuilt);
|
|
await settle([compareArtifacts({ ir: false, llvm: true }), probeRebuilt()]);
|
|
};
|
|
await runNativeBootstrapChecks(plan, {
|
|
commands: () => bootstrapStep("seed command and library probes", seedChecks),
|
|
rebuild: rebuildChecks,
|
|
"rebuild-frontend": rebuildFrontend,
|
|
"rebuild-emit": rebuildEmit,
|
|
});
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
},
|
|
sanitize ? 10_800_000 : 5_400_000,
|
|
);
|