Files
scriptc/tests/harness/self-hosting.test.ts
Chris Tate 3f66c8e20b Make LLVM the sole production backend
- Remove C emission, debugging options, and automatic backend fallback.
- Complete LLVM lowering and native bootstrap coverage across supported targets.
- Link executables and libraries with precompiled C runtime packs, including iOS and Android.
2026-09-29 20:54:09 -05:00

106 lines
5.5 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test } from "vitest";
import { analyze, compile, compileC, deserializeModule, emitLlvmModule, validateModule } from "@scriptc/compiler";
const root = fileURLToPath(new URL("../..", import.meta.url));
test("the complete IR validator lowers statically without skipped functions", () => {
const entry = join(root, "tests/fixtures/self-hosting/validate.ts");
const { coverage } = analyze(entry, { dynamic: false });
expect(coverage.preflightFailed).toBe(false);
expect(coverage.stats.statementsTotal).toBeGreaterThan(0);
expect(coverage.stats.statementsFailed).toBe(0);
expect(coverage.stats.statementsIsland).toBe(0);
expect(coverage.stats.functionsSkipped).toBe(0);
});
// Keep these outside the ordinary corpus: they import implementation files
// beyond the fixture directory, which the corpus oracle cache does not hash.
// Node executes the actual TS modules through tsx's .js → .ts resolution.
for (const component of ["source-locations", "ir-collections", "ir-types", "ir-control-flow", "emitter-literals"]) {
for (const backend of ["llvm"] as const) {
test(`self-hosting ${component}: ${backend} matches Node`, async () => {
const entry = join(root, "tests/fixtures/self-hosting", `${component}.ts`);
const outDir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-self-hosting-"));
try {
const oracle = spawnSync(process.execPath, ["--import", "tsx", entry], {
cwd: root, timeout: 30_000, maxBuffer: 1024 * 1024,
});
expect(oracle.error).toBeUndefined();
expect(oracle.signal).toBeNull();
expect(oracle.status, oracle.stderr.toString()).toBe(0);
expect(oracle.stdout.length).toBeGreaterThan(0);
const built = await compile(entry, {
outDir,
outPath: join(outDir, process.platform === "win32" ? "program.exe" : "program"),
backend,
dynamic: false,
sanitize: process.env["SCRIPTC_SAN"] === "1",
});
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
expect(built.backend).toBe(backend);
const native = spawnSync(built.binaryPath, [], { cwd: root, timeout: 30_000, maxBuffer: 1024 * 1024 });
expect(native.error).toBeUndefined();
expect(native.signal).toBeNull();
expect(native.status, native.stderr.toString()).toBe(oracle.status);
expect(native.stdout).toEqual(oracle.stdout);
expect(native.stderr).toEqual(oracle.stderr);
} finally {
rmSync(outDir, { recursive: true, force: true });
}
});
}
}
// This is a native compiler stage: the executable imports and runs the
// actual IR builder, analysis functions and validator, writes IR, and that
// IR must produce a working executable. Comparing its serialized IR
// to Node also pins construction order and every recursive payload.
for (const [fixture, backend] of ["ir-build", "contextual-ir"].flatMap((fixture) =>
(["llvm"] as const).map((backend) => [fixture, backend] as const))) {
test(`self-hosting IR generation ${fixture}: ${backend} builds and validates an executable program`, async () => {
const entry = join(root, "tests/fixtures/self-hosting", `${fixture}.ts`);
const outDir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-ir-build-"));
const sanitize = process.env["SCRIPTC_SAN"] === "1";
const exe = (name: string): string => join(outDir, name + (process.platform === "win32" ? ".exe" : ""));
try {
const built = await compile(entry, { outDir, outPath: exe("builder"), backend, dynamic: false, sanitize, optimization: "dev" });
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
expect(built.backend).toBe(backend);
for (const bound of [0, 6]) {
const args = [String(bound)];
const options = { cwd: root, timeout: 30_000, maxBuffer: 4 * 1024 * 1024 };
const oracle = spawnSync(process.execPath, ["--import", "tsx", entry, ...args], options);
const native = spawnSync(built.binaryPath, args, options);
for (const result of [oracle, native]) {
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status, result.stderr.toString()).toBe(0);
}
expect(native.stdout).toEqual(oracle.stdout);
expect(native.stderr).toEqual(oracle.stderr);
const mod = deserializeModule(native.stdout.toString());
expect(validateModule(mod)).toEqual([]);
const llvmPath = join(outDir, `generated-${bound}.ll`);
const outPath = exe(`generated-${bound}`);
writeFileSync(llvmPath, emitLlvmModule(mod));
await compileC({ cPath: llvmPath, outPath, sanitize });
const program = spawnSync(outPath, [], options);
expect(program.error).toBeUndefined();
expect(program.signal).toBeNull();
expect(program.status, program.stderr.toString()).toBe(0);
const label = fixture === "ir-build" ? "built" : "context";
const suffix = fixture === "ir-build" ? " true" : "";
expect(program.stdout.toString()).toBe(`${label}${"!".repeat(bound)} ${bound * (bound - 1) / 2}${suffix}\n`);
expect(program.stderr.toString()).toBe("");
}
} finally {
rmSync(outDir, { recursive: true, force: true });
}
});
}