mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
`Int32` from @scriptc/types now also annotates the element type of a declared number array (`Int32[]`, `readonly Int32[]`, `Array<Int32>`, `ReadonlyArray<Int32>`) on parameters, variables, class instance fields and function results. The contract promises that every element stored into the array, through any reference, is an int32 by construction. contracts/int32-elements.ts checks it over a whole-program flow graph of the places array references live (locals, parameters, globals, results, class field families, record fields, container element slots), with one node per allocation and per opaque origin. Allocations that can reach an annotated slot are constrained and every element store into an array that may be constrained must be range-proven an int32 (SC7002); element copies constrain their sources by fixpoint; arrays from or escaping to code the IR does not show refuse the contract (SC7003). Slots only constrained arrays reach are recorded as IrContracts.int32Elements. Element reads that find a value through those slots carry the int32 range, `getNumber` reads are integer-or-NaN operands of wrapping sums, and ToInt32 of any integer-or-NaN operand is branch-free. Holes, missing indices and length changes keep their JavaScript behavior. Integer kernels over a 2^20-element array: `(s + a[i]) | 0` 1086 -> 175 ms, `(Math.imul(h, 31) + a[i]) | 0` 602 -> 150 ms, prefix sums 575 -> 155 ms; programs without the annotation compile to identical binaries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import { mkdtemp, readFile, rm, symlink, 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";
|
|
|
|
const repoRoot = join(import.meta.dirname, "../../..");
|
|
|
|
const source = `
|
|
import type { Int32 } from "@scriptc/types";
|
|
function make(n: number): Int32[] {
|
|
const out: Int32[] = [];
|
|
for (let i = 0; i < n; i++) out.push((i * 7) & 255);
|
|
return out;
|
|
}
|
|
function sum(a: Int32[]): number {
|
|
let s = 0;
|
|
for (let i = 0; i < a.length; i++) s = (s + a[i]) | 0;
|
|
return s;
|
|
}
|
|
function xorAll(a: Int32[]): number {
|
|
let h = 0;
|
|
for (let i = 0; i < a.length; i++) h = h ^ a[i];
|
|
return h;
|
|
}
|
|
function plainSum(a: number[]): number {
|
|
let s = 0;
|
|
for (let i = 0; i < a.length; i++) s = (s + a[i]) | 0;
|
|
return s;
|
|
}
|
|
function first(a: Int32[]): number {
|
|
return a.length > 0 ? a[0] : -1;
|
|
}
|
|
const xs = make(100);
|
|
const floats = [0.5, 1.5];
|
|
console.log(sum(xs), xorAll(xs), plainSum(floats), plainSum([1, 2]), first(xs));
|
|
`;
|
|
|
|
const body = (ll: string, name: string): string =>
|
|
ll.match(
|
|
new RegExp(
|
|
`define internal [^\\n]+ @sc_(?:b)?f_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{ ;([\\s\\S]*?)\\n\\}`,
|
|
),
|
|
)![1]!;
|
|
|
|
test("Int32[] element reads compute integer kernels in 32 bits", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-arrays-"));
|
|
try {
|
|
await symlink(join(repoRoot, "node_modules"), join(dir, "node_modules"));
|
|
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"));
|
|
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
|
const facts = mod.contracts?.int32Elements ?? [];
|
|
expect(facts).toContain("l:sum\0a.0");
|
|
expect(facts).toContain("l:xorAll\0a.0");
|
|
expect(facts).toContain("r:make");
|
|
// The unannotated function also receives a float array: no fact.
|
|
expect(facts).not.toContain("l:plainSum\0a.0");
|
|
|
|
const ll = emitLlvmModule(mod, { pointerBits: 64, wasi: false });
|
|
// (s + a[i]) | 0 over an Int32[] element: a wrapping i32 add that
|
|
// selects 0 when the read found no value (NaN), with no ToInt32 slow
|
|
// path on the loop-carried accumulator.
|
|
const sum = body(ll, "sum");
|
|
expect(sum).toMatch(/ = add i32 /);
|
|
expect(sum).toMatch(/ = fcmp ord double /);
|
|
expect(sum).not.toContain("uint32.coerce.slow");
|
|
// h ^ a[i]: the element's ToInt32 is its low 32 bits, or 0 for NaN.
|
|
const xor = body(ll, "xorAll");
|
|
expect(xor).toMatch(/ = select i1 %\w+, i32 %\w+, i32 0/);
|
|
expect(xor).not.toContain("uint32.coerce.slow");
|
|
// A number[] that may hold fractions keeps the full conversion.
|
|
expect(body(ll, "plainSum")).toContain("uint32.coerce.slow");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|