mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
A while loop whose remainder reads number locals that enter it without range facts, such as copies of module globals, parameters or fields (`const base = column;` before `(count + base - breaks) % 60`), now runs as two copies. On entry, a test checks that each such local holds an integer in [-2^48, 2^48] other than -0. The fast copy uses the facts the integer range analysis proves when the function is analyzed again with those entry ranges assumed: the loop's counters become bounded, and the sums and remainders run as i64 arithmetic. When the test fails, the loop runs as it did before. - ir/loop-entry-ranges.ts picks innermost while loops with a remainder that the assumed ranges prove and the function's own analysis does not. The test covers only the locals the remainder depends on, directly, through the loop's writes or through its condition. - ir/integer-ranges.ts takes the assumed entry ranges per loop, and keeps no fact about the assumed locals or the loop's writes after it. - backend/llvm/loop-entry-versions.ts emits the test and both copies. The test is marked likely. A function called only from the loop gets `alwaysinline` on the fast copy's call, because the second copy would otherwise cost it the inlining of a single call site. reverse-complement (25M bases): 2.08 s -> 1.19 s; Node 3.97 s.
46 lines
2.3 KiB
TypeScript
46 lines
2.3 KiB
TypeScript
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
import { compile, deserializeModule } from "../src/index.js";
|
|
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
|
|
|
test("while loops test their entry values and run integer remainders in the fast copy", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-entry-versions-"));
|
|
try {
|
|
const entry = join(import.meta.dirname, "../../../tests/corpus/while-entry-range-versions.ts");
|
|
const outPath = join(dir, "versions.ir.json");
|
|
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
|
|
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
|
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
|
const ll = emitLlvmModule(mod, { pointerBits: 64 });
|
|
const body = (name: string): string =>
|
|
ll.match(
|
|
new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\{([\\s\\S]*?)\\n\\}`),
|
|
)![1]!;
|
|
for (const name of ["wrap", "ring", "_x25_Cursor_stripes"]) {
|
|
const fn = body(name);
|
|
const fast = fn.slice(fn.indexOf("\nentry.fast"), fn.indexOf("\nentry.slow"));
|
|
const slow = fn.slice(fn.indexOf("\nentry.slow"));
|
|
// The fast copy adds the dividend's integers; the ordinary copy adds
|
|
// doubles and tests the sum before its integer remainder.
|
|
const adds = (text: string): number => text.split("fadd double").length - 1;
|
|
expect(fast, name).toContain("srem i64");
|
|
expect(adds(fast), name).toBeLessThan(adds(slow));
|
|
}
|
|
// `complement` has no other caller: the fast copy's call keeps it inlined.
|
|
const wrap = body("wrap");
|
|
const fast = wrap.slice(wrap.indexOf("\nentry.fast"), wrap.indexOf("\nentry.slow"));
|
|
expect(fast).toMatch(/call i32 @sc_f_complement\.i32\([^)]*\) alwaysinline/);
|
|
expect(wrap.slice(wrap.indexOf("\nentry.slow"))).not.toContain("alwaysinline");
|
|
// Debug builds keep one copy.
|
|
const debug = emitLlvmModule(mod, {
|
|
pointerBits: 64,
|
|
debugSources: new Map([[entry, await readFile(entry, "utf8")]]),
|
|
});
|
|
expect(debug).not.toContain("entry.fast");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|