mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
A typed-array loop version in a worker program tested its bounds and its views' ownership together: every view had to have no shared storage. A loop over views of a SharedArrayBuffer always failed that guard and ran the checked copy, testing every element's index. The guard now tests the bounds first and the ownership after. When the bounds hold and every view is unshared, the loop runs the owned copy as before. When a view is shared, it runs a new third copy whose accesses skip their bounds checks and keep the shared-or-local path, without alias scopes. Versions with a UTF-16 units copy, and programs without workers, are unchanged. Corpus 8350 runs such loops over shared, unshared and mixed views, to counts within and past the views and over subarrays, with a worker writing through shared views. The emission test checks the split guard and the shared copy's accesses.
75 lines
2.7 KiB
TypeScript
75 lines
2.7 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, deserializeModule } from "../src/index.js";
|
|
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
|
|
|
async function emitted(source: string): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-worker-shared-loop-"));
|
|
try {
|
|
const entry = join(dir, "main.ts");
|
|
const outPath = join(dir, "main.ir.json");
|
|
await writeFile(entry, source);
|
|
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
|
if (!result.ok)
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const kernel = `
|
|
function sumTo(view: Float64Array, n: number): number {
|
|
let total = 0;
|
|
for (let i = 0; i < n; i++) total += view[i]!;
|
|
return total;
|
|
}
|
|
`;
|
|
|
|
function body(ll: string, name: string): string {
|
|
return new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
|
|
ll,
|
|
)![0];
|
|
}
|
|
|
|
/** The instructions of the blocks whose labels start with `prefix`. */
|
|
function blocks(fn: string, prefix: string): string {
|
|
return fn
|
|
.split(/\n(?=[\w.$-]+:)/)
|
|
.filter((block) => block.startsWith(prefix))
|
|
.join("\n");
|
|
}
|
|
|
|
test("a worker program's loop over views that may be shared keeps a bounds-proven copy", async () => {
|
|
const ll = await emitted(`
|
|
import { Worker, isMainThread } from "node:worker_threads";
|
|
${kernel}
|
|
if (isMainThread) {
|
|
const view = new Float64Array(new SharedArrayBuffer(64));
|
|
console.log(sumTo(view, 8));
|
|
new Worker(new URL(import.meta.url));
|
|
}
|
|
`);
|
|
const fn = body(ll, "sumTo");
|
|
// The guard tests the bounds first, then whether the view is shared.
|
|
expect(fn).toMatch(/^bytes\.version\.owned\d*:/m);
|
|
expect(fn).toMatch(/^bytes\.version\.fast\.shared\d*:/m);
|
|
// The shared copy reads with no bounds test, and shared-safe: a view
|
|
// with shared storage loads atomically.
|
|
const shared = blocks(fn, "bytes.version.fast.shared");
|
|
expect(shared).not.toMatch(/icmp (ult|uge|ugt) i64 %\w+, %\w+/);
|
|
expect(fn).toMatch(/load atomic double, ptr %\w+ monotonic/);
|
|
});
|
|
|
|
test("a program without workers keeps two copies of the loop", async () => {
|
|
const ll = await emitted(`${kernel}
|
|
console.log(sumTo(new Float64Array(8), 8));
|
|
`);
|
|
const fn = body(ll, "sumTo");
|
|
expect(fn).toMatch(/^bytes\.version\.fast\d*:/m);
|
|
expect(fn).not.toMatch(/bytes\.version\.fast\.shared/);
|
|
expect(fn).not.toMatch(/bytes\.version\.owned/);
|
|
});
|