mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
`length` and `byteLength` loaded the typed array's header with `!invariant.load` (#2016). LLVM may move such a load down to its use, past the release that frees the view, so `const slots = this.slots; this.slots = next; return slots.length` returned a garbage number in release builds. These reads are now ordinary loads that keep the header's alias tag. Header loads that only bound or address an element access stay invariant: their values reach only that access, which runs while the view is alive, so typed-array loops still hoist the length and data pointer. Corpus typed-array-length-after-release covers a replaced field, a deleted map entry and a rebound global.
61 lines
2.2 KiB
TypeScript
61 lines
2.2 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, validateModule } from "../src/index.js";
|
|
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
|
import { type IrModule } from "../src/ir/ir.js";
|
|
|
|
async function lower(source: string): Promise<IrModule> {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-header-"));
|
|
try {
|
|
const entry = join(dir, "main.ts"),
|
|
output = join(dir, "main.ir.json");
|
|
await writeFile(entry, source);
|
|
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
|
if (!result.ok)
|
|
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
|
const module = deserializeModule(await readFile(output, "utf8"));
|
|
expect(validateModule(module)).toEqual([]);
|
|
return module;
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function body(llvm: string, symbol: string): string {
|
|
const found = new RegExp(`^define internal [^\\n]*@${symbol}(?:\\.i32)?\\([^]*?^}`, "m").exec(
|
|
llvm,
|
|
);
|
|
expect(found, symbol).not.toBeNull();
|
|
return found![0];
|
|
}
|
|
|
|
const INVARIANT_HEADER = /load (?:i64|i32|ptr), ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m;
|
|
|
|
test("a typed array's length read as a value is an ordinary load; access bounds stay invariant", async () => {
|
|
const module = await lower(`
|
|
class Table {
|
|
slots: Int32Array = new Int32Array(4);
|
|
swap(): number {
|
|
const slots = this.slots;
|
|
this.slots = new Int32Array(1);
|
|
return slots.length + slots.byteLength;
|
|
}
|
|
}
|
|
function sum(values: Float64Array, scale: number): number {
|
|
let total = 0;
|
|
for (let i = 0; i < values.length; i++) total += values[i]! * scale;
|
|
return total;
|
|
}
|
|
const table = new Table();
|
|
console.log(table.swap(), sum(new Float64Array([1, 2]), 2));
|
|
`);
|
|
const llvm = emitLlvmModule(module);
|
|
// The length outlives the view: the release must not be crossed.
|
|
const swap = body(llvm, "sc_bf__x25_Table_swap");
|
|
expect(swap).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+$/m);
|
|
expect(swap).not.toContain("!invariant.load");
|
|
expect(body(llvm, "sc_bf_sum")).toMatch(INVARIANT_HEADER);
|
|
});
|