mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
- 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.
106 lines
5.5 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
}
|