mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
A callback loop version emits its loop twice. A loop inside either copy could be versioned again, so a nest of loops that each call a callback built just before them (`const g = adder(i)`) doubled at every level: a three-deep nest was emitted seven times per function. A loop inside a callback version, or the versioned loop itself emitted again for each copy, is no longer versioned, so a loop nest is emitted at most twice. The outer loop keeps its version; inner loops keep their calls as they are.
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
import { compile } from "../src/index.js";
|
|
|
|
async function emitLlvm(source: string): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-callback-versions-"));
|
|
try {
|
|
const entry = join(dir, "main.ts");
|
|
const outPath = join(dir, "main.ll");
|
|
await writeFile(entry, source);
|
|
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
|
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
|
return await readFile(outPath, "utf8");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const body = (llvm: string, marker: string): string => {
|
|
const match = new RegExp(`^define internal [^\\n]*${marker}[^]*?^}`, "m").exec(llvm);
|
|
if (!match) throw new Error(`no function ${marker}`);
|
|
return match[0];
|
|
};
|
|
|
|
test("loops that call a known callback test its closure once and call it directly", async () => {
|
|
const llvm = await emitLlvm(`
|
|
function each(values: number[], visit: (v: number) => void): void {
|
|
for (let i = 0; i < values.length; i++) visit(values[i]!);
|
|
}
|
|
let sum = 0;
|
|
each([1, 2, 3], (v) => { sum += v; });
|
|
console.log(sum);
|
|
`);
|
|
const fn = body(llvm, "@sc_bf_each");
|
|
const fast = /^callback\.version\.fast\d+:$[^]*?^callback\.version\.done\d+:$/m.exec(fn)![0];
|
|
// The guard compares the closure's code pointer once, before the loop.
|
|
expect(fn.slice(0, fn.indexOf(fast))).toMatch(/icmp eq ptr %\S+, @/);
|
|
expect(fast).not.toContain("callback.indirect");
|
|
expect(fast).not.toContain("scr_dyn_this_push_dyn");
|
|
// The ordinary copy keeps the fallback through the closure's pointer.
|
|
expect(fn).toContain("callback.indirect");
|
|
});
|
|
|
|
test("captured number payloads carry their own alias tag", async () => {
|
|
const llvm = await emitLlvm(`
|
|
function total(values: number[]): number {
|
|
let sum = 0;
|
|
values.forEach((v) => { sum += v; });
|
|
return sum;
|
|
}
|
|
console.log(total([1, 2, 3]));
|
|
`);
|
|
// Every read and write through a box's payload slot (field 5) is tagged.
|
|
const slots = [
|
|
...llvm.matchAll(/(%\S+) = getelementptr inbounds %ScrBox, ptr %\S+, i32 0, i32 5/g),
|
|
];
|
|
expect(slots.length).toBeGreaterThan(0);
|
|
for (const [, slot] of slots) {
|
|
for (const access of llvm.matchAll(
|
|
new RegExp(`(load double, ptr|store double %\\S+, ptr) \\${slot}\\b[^\\n]*`, "g"),
|
|
))
|
|
expect(access[0]).toMatch(/!tbaa !\d+$/);
|
|
}
|
|
expect(llvm).toMatch(/load double, ptr %\S+, !tbaa !\d+/);
|
|
});
|
|
|
|
test("a callback declared in the loop body is not read before the loop", async () => {
|
|
const llvm = await emitLlvm(`
|
|
function strides(): number {
|
|
let calls = 0;
|
|
for (let i = 0; i < 10; i++) {
|
|
const bump = (): void => { i += 2; };
|
|
bump();
|
|
calls++;
|
|
}
|
|
return calls;
|
|
}
|
|
console.log(strides());
|
|
`);
|
|
expect(llvm).not.toContain("callback.version");
|
|
});
|
|
|
|
test("loops inside a callback version are not versioned again", async () => {
|
|
// Each inner loop calls a callback a factory builds just before it; without
|
|
// the limit, every copy of a version would version the next loop again.
|
|
const llvm = await emitLlvm(`
|
|
function adder(k: number): (v: number) => number {
|
|
return (v: number) => v + k;
|
|
}
|
|
function nest(n: number, f: (v: number) => number): number {
|
|
let s = 0;
|
|
for (let i = 0; i < n; i++) {
|
|
s += f(i);
|
|
const g = adder(i);
|
|
for (let j = 0; j < n; j++) {
|
|
s += g(j);
|
|
const h = adder(j);
|
|
for (let k = 0; k < n; k++) s += h(k);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
console.log(nest(3, (v) => v - 1), nest(2, (v) => v * 3));
|
|
`);
|
|
// One version for the outer loop of each compiled `nest`; its two copies
|
|
// keep the inner loops whole.
|
|
const versions = [...llvm.matchAll(/^define [^\n]*@sc_bf_nest[^]*?^}/gm)].map(
|
|
([body]) => body.match(/^callback\.version\.fast\d+:$/gm)?.length ?? 0,
|
|
);
|
|
expect(versions.every((count) => count <= 1)).toBe(true);
|
|
expect(versions.some((count) => count === 1)).toBe(true);
|
|
});
|