Files
scriptc/packages/compiler/test/parameter-version-emission.test.ts
T
Chris Tate 6681b9407e Version functions on number parameters that bound their loops
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.
2026-10-22 11:46:21 -05:00

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([]);
});