Files
scriptc/packages/cli/test/exceptions.test.ts
T
Chris Tate 377af617d3 Add --exceptions=rare to unwind through recursive functions
With --exceptions=rare, recursive functions and functions up to three
calls above a throw also propagate exceptions by unwinding on Linux, so
calls to them need no pending-flag test. Functions with a throw of their
own keep the flag. The default, common, keeps the current rules, and
builds without the flag emit the same code as before.

The option is part of the executable cache key; the bootstrap's cached
route leaves builds with it to the full compiler, and coverage and
library builds refuse it.
2026-10-28 14:28:28 -05:00

95 lines
3.6 KiB
TypeScript

import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
// A recursion that a throw below it crosses: with --exceptions=rare, walk
// calls itself with invoke.
const program = `function leaf(n: number): number {
if (n % 7 === 0) throw new RangeError("seven " + n);
return n;
}
function walk(n: number, depth: number): number {
const parts: string[] = ["w" + n];
const r = depth === 0 ? leaf(n) : walk(n, depth - 1);
return r + parts.length;
}
let sum = 0;
for (let i = 1; i < 40; i++) {
try { sum += walk(i, 20); } catch (e) { console.log((e as Error).message); }
}
console.log(sum);
`;
test.runIf(process.platform === "linux")(
"--exceptions=rare unwinds through recursive functions without mixing cache variants",
async () => {
const dir = await mkdtemp(join("/tmp", "scriptc-exceptions-"));
const entry = join(dir, "main.ts");
const outPath = join(dir, "program");
const llvmPath = join(dir, "main.ll");
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
const build = async (args: string[], bootstrap = false): Promise<string> => {
await execFileAsync(
process.execPath,
[
...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
"build",
entry,
"-o",
outPath,
...args,
],
{ env, maxBuffer: 4 * 1024 * 1024 },
);
return readFile(llvmPath, "utf8");
};
const walkInvokes = (llvm: string): boolean =>
/^define internal [^\n]*@sc_b?f_walk\([^]*?invoke double @sc_b?f_walk[^]*?^}/m.test(llvm);
try {
await writeFile(entry, program);
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
const common = await build([]);
expect(walkInvokes(common)).toBe(false);
expect(await build(["--exceptions=common"])).toBe(common);
const rare = await build(["--exceptions=rare"]);
expect(walkInvokes(rare)).toBe(true);
expect((await execFileAsync(outPath)).stdout).toBe(expected);
// The bootstrap's cached route builds with the default rules.
expect(await build([], true)).toBe(common);
expect((await execFileAsync(outPath)).stdout).toBe(expected);
await expect(build(["--exceptions=never"])).rejects.toThrow(
/unknown exception frequency "never"/,
);
} finally {
await rm(dir, { recursive: true, force: true });
}
},
240_000,
);
test("--exceptions is a build/run option", async () => {
const dir = await mkdtemp(join("/tmp", "scriptc-exceptions-coverage-"));
const entry = join(dir, "main.ts");
try {
await writeFile(entry, program);
const coverage = execFileAsync(
process.execPath,
["--import", tsxLoader, cliEntry, "coverage", entry, "--exceptions=rare"],
{ env: { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") } },
);
await expect(coverage).rejects.toThrow(/--exceptions is a build\/run option/);
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 120_000);