mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Keep loop-carried locals written from int32 element reads int32
The int32 array proof assumes its candidates' elements are int32 and asks the integer range analysis whether every stored value is one. A local assigned in a loop from an element read of a candidate array (`const t = next[at]!; next[at] = at; at = t`) never closed under the int32 bound, because the loop-carried closure had no case for element reads, so arrays that store such cursors kept double slots and every index converted from a double. Element reads now take the slot range in that closure, as field reads and calls already did. The index-cursors runtime workload walks an array of indices with such a cursor.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/index-cursors.ts",
|
||||
"args": [
|
||||
"20000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Arrays of indices walked by loop-carried cursors: each step reads the
|
||||
// next index from the array and relinks the visited entries, so the
|
||||
// cursor and every stored value are small integers read back from the
|
||||
// array itself.
|
||||
const N = 4096;
|
||||
|
||||
function relink(rounds: number): number {
|
||||
const next: number[] = [];
|
||||
for (let i = 0; i < N; i++) next.push(i + 1 < N ? i + 1 : 0);
|
||||
let at = next[0]!;
|
||||
let total = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let s = 0; s < N; s++) {
|
||||
const t = next[at]!;
|
||||
next[at] = next[t]!;
|
||||
next[t] = at;
|
||||
at = t;
|
||||
total = (total + at) | 0;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "1");
|
||||
console.log("relink", relink(rounds));
|
||||
@@ -440,6 +440,37 @@ test("int32-closed locals keep the int32 range across loops and switches", () =>
|
||||
expect(switched.get(afterSwitch)).toEqual(INT32);
|
||||
});
|
||||
|
||||
test("loop-carried locals written from Int32[] element reads keep the int32 range", () => {
|
||||
const arrType = { kind: "array", elem: F64 } as const;
|
||||
const loop = (localId: string): { after: IrExpr; f: IrFunction } => {
|
||||
const after = ref();
|
||||
// `x = a[x]!` in a loop: a cursor that follows indices stored in the array.
|
||||
const read: IrExpr = {
|
||||
loc,
|
||||
kind: "arrayGet",
|
||||
arr: { loc, kind: "varRef", localId, type: arrType },
|
||||
index: ref(),
|
||||
type: F64,
|
||||
};
|
||||
const f = fn([
|
||||
assign(num(0)),
|
||||
{
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("<", ref("unknown"), num(1)),
|
||||
body: [assign(read)],
|
||||
},
|
||||
observe(after),
|
||||
]);
|
||||
return { after, f };
|
||||
};
|
||||
const elements = { elements: (arr: IrExpr) => arr.kind === "varRef" && arr.localId === "ints" };
|
||||
const ints = loop("ints");
|
||||
expect(analyzeIntegerRanges(ints.f, elements).get(ints.after)).toEqual(INT32);
|
||||
const floats = loop("floats");
|
||||
expect(analyzeIntegerRanges(floats.f, elements).get(floats.after) ?? null).toBeNull();
|
||||
});
|
||||
|
||||
test("uint32-closed locals keep the uint32 range across loop back edges", () => {
|
||||
// for (let i = 0; i < n; i++) { v = unknown >>> 0; acc = (acc + v) >>> 0; ... }
|
||||
const loop = (start: IrExpr | null, writes: IrExpr[]) => {
|
||||
|
||||
@@ -480,6 +480,7 @@ export function analyzeIntegerRangeFacts(
|
||||
return boxedRange(e.localId);
|
||||
}
|
||||
case "fieldGet":
|
||||
case "arrayGet":
|
||||
case "call":
|
||||
return slotRange(e);
|
||||
default:
|
||||
|
||||
@@ -77,6 +77,19 @@ function literalCounter(n: number): number {
|
||||
for (let i = 0; i < n; i++) a[0] = a[0]! + 1;
|
||||
return a[0]!;
|
||||
}
|
||||
function walk(n: number, steps: number): number {
|
||||
const next: number[] = [];
|
||||
for (let i = 0; i < n; i++) next.push(i + 1 < n ? i + 1 : 0);
|
||||
let at = next[0]!;
|
||||
let total = 0;
|
||||
for (let s = 0; s < steps; s++) {
|
||||
const t = next[at]!;
|
||||
next[at] = at;
|
||||
at = t;
|
||||
total = (total + at) | 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
function joined(n: number): string {
|
||||
const a = [n | 0, 2];
|
||||
return a.join(",");
|
||||
@@ -91,6 +104,7 @@ console.log(
|
||||
shortCounter(),
|
||||
literalCounter(n),
|
||||
joined(n),
|
||||
walk(n | 0, 5),
|
||||
);
|
||||
`;
|
||||
|
||||
@@ -119,6 +133,16 @@ test("arrays storing only int32 elements get 4-byte slots", async () => {
|
||||
expect(counts).not.toContain("popArray");
|
||||
});
|
||||
|
||||
test("an array that stores a cursor read back from its own elements gets 4-byte slots", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const walk = functions(llvm, "walk");
|
||||
expect(walk).not.toBe("");
|
||||
// `at` is assigned `next[at]!` in the loop and stored back into `next`:
|
||||
// it holds an int32 whenever the array's elements are int32s.
|
||||
expect(walk).toMatch(/@scr_arr_new_small\(i32 7\)/);
|
||||
expect(walk).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/);
|
||||
});
|
||||
|
||||
test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
for (const name of ["fraction", "negativeZero", "joined"]) {
|
||||
|
||||
Reference in New Issue
Block a user