Files
scriptc/tests/harness/atomics-emission.test.ts
T
Chris Tate 93bc902cd8 Emit Atomics operations inline as atomic instructions
Atomics.load, store, exchange, compareExchange, add, sub, and, or and xor
on an integer typed array compile to one sequentially consistent LLVM
atomic instruction on the element. An index that is not an integer in
range takes the runtime operation, which applies ToIndex and raises its
errors.
2026-10-16 00:31:52 -05:00

64 lines
2.9 KiB
TypeScript

/* Atomics emission (behavior is in the differential corpus, 5740-5746):
* operations on integer typed arrays compile to sequentially consistent
* atomic instructions on the element, and only an index that is not an
* integer in range calls the runtime operation. */
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const repoRoot = join(import.meta.dirname, "../..");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
async function emittedLlvm(name: string, source: string): Promise<string> {
const key = createHash("sha256").update(source).digest("hex").slice(0, 16);
const outDir = join(cacheDir, `atomics-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, `${name}.ts`);
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, name),
outDir,
backend: "llvm",
outputKind: "llvm",
});
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
return readFileSync(result.artifact.path, "utf8");
}
function body(llvm: string, name: string): string {
const start = llvm.search(new RegExp(`^define internal [^\\n]*@${name}[.(]`, "m"));
expect(start).toBeGreaterThanOrEqual(0);
return llvm.slice(start, llvm.indexOf("\n}\n", start));
}
test("Atomics operations compile to sequentially consistent instructions", async () => {
const llvm = await emittedLlvm(
"ops",
`function work(a: Int32Array, b: Uint8Array, c: Int16Array, i: number): number {
let t = Atomics.add(a, i, 1) + Atomics.sub(a, i, 2) + Atomics.and(a, i, 3);
t += Atomics.or(b, i, 4) + Atomics.xor(b, i, 5) + Atomics.exchange(c, i, 6);
t += Atomics.compareExchange(a, i, 7, 8) + Atomics.load(b, i) + Atomics.store(c, i, 9);
return t;
}
const buffer = new SharedArrayBuffer(16);
console.log(work(new Int32Array(buffer), new Uint8Array(buffer), new Int16Array(buffer), 1));
`,
);
const work = body(llvm, "sc_bf_work");
for (const op of ["add", "sub", "and"])
expect(work).toMatch(new RegExp(`atomicrmw ${op} ptr %t\\d+, i32 [^,]+ seq_cst, align 4`));
for (const op of ["or", "xor"])
expect(work).toMatch(new RegExp(`atomicrmw ${op} ptr %t\\d+, i8 [^,]+ seq_cst, align 1`));
expect(work).toMatch(/atomicrmw xchg ptr %t\d+, i16 [^,]+ seq_cst, align 2/);
expect(work).toMatch(/cmpxchg ptr %t\d+, i32 [^,]+, i32 [^,]+ seq_cst seq_cst, align 4/);
expect(work).toMatch(/load atomic i8, ptr %t\d+ seq_cst, align 1/);
expect(work).toMatch(/store atomic i16 [^,]+, ptr %t\d+ seq_cst, align 2/);
// Every runtime call is the cold path for an index out of range.
const calls = work.match(/call double @scr_atomics_op\([^\n]*/g) ?? [];
expect(calls).toHaveLength(9);
for (const call of calls) expect(call).toMatch(/\) cold$/);
});