Files
scriptc/packages/compiler/test/loop-entry-versions.test.ts
T
Chris Tate fb58a34ca1 Version for loops on the positions they advance
- Loop-entry versioning covers innermost for loops as well as while loops,
  and a typed array index that the assumed entry ranges prove qualifies a
  loop as a remainder does. In `for (let j = 0; j < line; j++)
  out[o++] = f(src[i++])` the test checks `line`, `o` and `i` once per
  entry, and the fast copy bounds both positions by the loop's passes.
- Proven integer loops step their shadowed bindings in expression position
  too (`out[o++] = v`), so those positions run as i64 counters in the
  fast copy and the typed array accesses use them without a round trip.
- An increment of a binding the enclosing for loop bounds takes the loop
  range where a surrounding statement has discarded its fact.
2026-10-27 07:44:34 -05:00

76 lines
3.8 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 });
}
});
test("for loops test the positions they advance and step them as integers in the fast copy", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-entry-cursors-"));
try {
const entry = join(import.meta.dirname, "../../../tests/corpus/for-entry-range-cursors.ts");
const outPath = join(dir, "cursors.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 ["lines", "reverse", "find"]) {
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 steps i64 shadows of the positions; the ordinary copy
// steps their doubles.
expect(fast, name).toMatch(/= (add|sub) nsw i64 %t\d+, 1\n/);
expect(fn, name).toMatch(/alloca i64 ; integer [io]\n/);
expect(slow, name).toMatch(/= f(add|sub) double %t\d+, 0x3FF0000000000000\n/);
}
// The initializer writes a position, so the loop has one copy.
expect(body("restart")).not.toContain("entry.fast");
} finally {
await rm(dir, { recursive: true, force: true });
}
});