mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
A number parameter that every call site proves an int32 reaches the function's i32 entry. One that some caller passes as an unproven number, such as a value read from a worker message or parsed from text, kept its double, so every loop it bounds compared an integer counter with a double limit and the values derived from it stayed doubles. When such a parameter bounds a numeric loop (arithmetic, element and field reads and writes, Math calls, no allocation or other calls), the function now gets a copy of its body. Its own body first tests each such parameter for an int32 that is not -0 and, when all pass, calls the copy with `p | 0`, the same double. The copy's only caller proves those parameters int32s, so it gets the i32 entry and integer loops. Other values run the body as before. Only plain synchronous functions of at most 2048 IR nodes are versioned: not class members or generated helpers, not capturing, not the entry, not dynamic-tier, not calling themselves, and at most 16 per module. A function holding the only call site of another function is kept whole unless that callee only computes a number or boolean from scalar reads: a second call site can stop LLVM from inlining the callee and leave the records it builds in memory. Library builds and modules that observe call stacks are left as they are.
72 lines
2.4 KiB
TypeScript
72 lines
2.4 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-parameter-version-"));
|
|
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" });
|
|
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
|
return await readFile(outPath, "utf8");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const versions = (llvm: string): string[] => [
|
|
...new Set([...llvm.matchAll(/^define [^@]*@sc_\w+?_(\w+?)_int32Params\b/gm)].map((m) => m[1]!)),
|
|
];
|
|
|
|
const loops = `
|
|
function sumTo(n: number): number {
|
|
let s = 0;
|
|
for (let i = 0; i < n; i++) s += i;
|
|
return s;
|
|
}
|
|
function literal(n: number): number {
|
|
let s = 0;
|
|
for (let i = 0; i < n; i++) s += i;
|
|
return s;
|
|
}
|
|
function recursive(n: number): number {
|
|
let s = 0;
|
|
for (let i = 0; i < n; i++) s += i;
|
|
return n > 0 ? s + recursive(n - 1) : s;
|
|
}
|
|
function unbounded(n: number): number {
|
|
let s = 0;
|
|
for (let i = 0; i < 10; i++) s += n;
|
|
return s;
|
|
}
|
|
class Counter {
|
|
total(n: number): number {
|
|
let s = 0;
|
|
for (let i = 0; i < n; i++) s += i;
|
|
return s;
|
|
}
|
|
}
|
|
const n = Number(process.argv.length > 5 ? "7.5" : "6");
|
|
console.log(sumTo(n), literal(4), literal(5), recursive(n), unbounded(n), new Counter().total(n));
|
|
`;
|
|
|
|
test("a loop-bounding parameter some caller passes unproven gets an int32 version", async () => {
|
|
const llvm = await emitLlvm(loops);
|
|
expect(versions(llvm)).toEqual(["sumTo"]);
|
|
// The version takes the parameter as an i32, and the body dispatches to it
|
|
// after testing for an int32 that is not -0.
|
|
expect(llvm).toMatch(/^define [^@]*@sc_\w+_sumTo_int32Params\.i32\(i32 /m);
|
|
const body = /^define [^@]*@sc_\w+?_sumTo\([^]*?^}/m.exec(llvm)?.[0] ?? "";
|
|
expect(body).toMatch(/call [^@]*@sc_\w+_sumTo_int32Params/);
|
|
expect(body).toMatch(/fdiv double 0x3FF0000000000000, %\S+/);
|
|
});
|
|
|
|
test("a module that reads stacks keeps a single body", async () => {
|
|
const llvm = await emitLlvm(`${loops}\nconsole.log(typeof new Error("x").stack);\n`);
|
|
expect(versions(llvm)).toEqual([]);
|
|
});
|