Files
scriptc/tests/harness/self-hosting.test.ts
Chris Tate c08ab955ae Enforce linting and consistent source formatting
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks.
- Preserve compiler import boundaries and protected fixtures while cleaning up lint findings.
- Standardize source formatting and validate import, fixture, and embedded-source protections.
2026-10-05 17:51:24 -05:00

158 lines
6.4 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",
"ir-traversal",
"union-discriminants",
"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", "coercion-builders"].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);
}
if (fixture === "coercion-builders") {
// Captured functions use the native record's declaration order.
// JSON member order is not part of the IR format.
expect(deserializeModule(native.stdout.toString())).toEqual(
deserializeModule(oracle.stdout.toString()),
);
} else {
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);
if (fixture === "coercion-builders") {
expect(program.stdout.toString()).toBe(
`${Array.from({ length: bound }, (_, i) => i * 2).join(",")} ${bound * 2} ${bound * 2}\n`,
);
} else {
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 });
}
});
}