Files
scriptc/packages/compiler/test/int32-array-contracts.test.ts
T
Malte UblandClaude Opus 5.5 24c54fc5d3 Add Int32[] element contracts
`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>
2026-10-17 12:08:06 -07:00

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 });
}
});