mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
Prove array lengths stable in index loops and version dense array loops
Index loops over arrays left the counter a double in several common shapes, and every element access at the counter tested the array's hole-free prefix on each pass: - A syntactic proof (ir/array-length-stability.ts) shows that a loop never changes an array's length: the body runs no user code, never rebinds the array or lets it escape, and only resizes local arrays that no other binding can hold. Element writes at a counter kept below the length are accepted. - Counted loops take a stable `a.length` as their limit, and an element access at the counter makes an unversioned counted loop worthwhile, so strided (`i += 2`) and descending (`i = a.length - 1; i >= 0`) loops get an integer counter. - Array lengths bound while-loop and for-loop counters by their range, which needs no stability proof. - A loop whose array length is stable and whose indexed arrays keep their storage runs as two copies. One test before the loop (the length is at most 2^31 - 1 and at most each indexed array's `filled` bound) selects a fast copy whose element accesses at the counter skip the prefix test and whose counter is an int32. LLVM can vectorize that copy. - Loop counters whose ranges fit in 32 bits convert to double from their low half, so an inlined callee's index conversion folds away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ce668b213
commit
714ef5aa09
@@ -0,0 +1,167 @@
|
||||
// Index loops bounded by an array's length whose bodies resize that array,
|
||||
// directly, through an alias, a helper or a closure: the bound is re-read on
|
||||
// every pass, exactly like Node.
|
||||
|
||||
function pushInBody(): string {
|
||||
const a = [1, 2, 3];
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
seen.push(a[i]);
|
||||
if (a.length < 6) a.push(a[i] * 10);
|
||||
}
|
||||
return `${seen.join(",")} | ${a.length}`;
|
||||
}
|
||||
|
||||
function stridedPush(): string {
|
||||
const a = [0, 1, 2, 3];
|
||||
let visits = 0;
|
||||
for (let i = 0; i < a.length; i += 2) {
|
||||
visits++;
|
||||
if (a.length < 9) a.push(i);
|
||||
}
|
||||
return `${visits} ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function truncateInBody(): string {
|
||||
const a = [5, 6, 7, 8, 9, 10];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
if (i === 2) a.length = 4;
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function growLengthInBody(): string {
|
||||
const a = [1, 2];
|
||||
let holes = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] === undefined) holes++;
|
||||
if (i === 0) a.length = 5;
|
||||
}
|
||||
return `${holes} ${a.length}`;
|
||||
}
|
||||
|
||||
function helperPush(xs: number[], v: number): void {
|
||||
if (xs.length < 7) xs.push(v);
|
||||
}
|
||||
|
||||
function pushThroughHelper(): string {
|
||||
const a = [1, 2, 3];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
helperPush(a, i + 100);
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function pushThroughClosure(): string {
|
||||
const a = [1, 2, 3];
|
||||
const grow = (v: number): void => {
|
||||
if (a.length < 6) a.push(v);
|
||||
};
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
grow(i);
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function pushThroughAlias(): string {
|
||||
const a = [1, 2, 3];
|
||||
const alias = a;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
if (alias.length < 8) alias.push(i * 2);
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function aliasedParameters(a: number[], out: number[]): string {
|
||||
// `out` may be `a` itself.
|
||||
let n = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
n++;
|
||||
if (out.length < 10) out.push(a[i]);
|
||||
}
|
||||
return `${n} ${a.length} ${out.length}`;
|
||||
}
|
||||
|
||||
function writePastEnd(): string {
|
||||
const a = [1, 2, 3];
|
||||
let passes = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
passes++;
|
||||
if (i + 1 === a.length && a.length < 6) a[i + 1] = i;
|
||||
}
|
||||
return `${passes} ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function shiftInBody(): string {
|
||||
const a = [1, 2, 3, 4, 5, 6];
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
seen.push(a[i]);
|
||||
a.shift();
|
||||
}
|
||||
return `${seen.join(",")} | ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function spliceInDescendingLoop(): string {
|
||||
const a = [1, 2, 3, 4, 5, 6, 7];
|
||||
for (let i = a.length - 1; i >= 0; i--) if (a[i] % 2 === 0) a.splice(i, 1);
|
||||
return a.join(",");
|
||||
}
|
||||
|
||||
function whilePush(): string {
|
||||
const a = [3, 1];
|
||||
let i = 0;
|
||||
let sum = 0;
|
||||
while (i < a.length) {
|
||||
sum += a[i];
|
||||
if (a.length < 5) a.push(sum);
|
||||
i++;
|
||||
}
|
||||
return `${sum} ${i} ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function nestedResize(): string {
|
||||
const rows = [[1], [2, 3], [4, 5, 6]];
|
||||
let total = 0;
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
const row = rows[r];
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
total += row[c];
|
||||
if (row.length < 3) row.push(c);
|
||||
}
|
||||
}
|
||||
return `${total} ${rows.map((row) => row.length).join(",")}`;
|
||||
}
|
||||
|
||||
function pushIntoOther(): string {
|
||||
const a = [2, 4, 6];
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < a.length; i++) out.push(a[i] + 1);
|
||||
for (let i = 0; i < out.length; i += 2) out.push(-1);
|
||||
return `${out.join(",")}`;
|
||||
}
|
||||
|
||||
console.log(pushInBody());
|
||||
console.log(stridedPush());
|
||||
console.log(truncateInBody());
|
||||
console.log(growLengthInBody());
|
||||
console.log(pushThroughHelper());
|
||||
console.log(pushThroughClosure());
|
||||
console.log(pushThroughAlias());
|
||||
const shared = [7, 8, 9];
|
||||
console.log(aliasedParameters(shared, shared));
|
||||
console.log(aliasedParameters([7, 8, 9], []));
|
||||
console.log(writePastEnd());
|
||||
console.log(shiftInBody());
|
||||
console.log(spliceInDescendingLoop());
|
||||
console.log(whilePush());
|
||||
console.log(nestedResize());
|
||||
console.log(pushIntoOther());
|
||||
@@ -0,0 +1,133 @@
|
||||
// Index loops over sparse arrays with lengths beyond 2^31, holes, and arrays
|
||||
// that become sparse while a loop runs. Dense-loop fast paths must leave all
|
||||
// of these on the general path.
|
||||
|
||||
function partialAscending(a: number[], limit: number): string {
|
||||
let sum = 0;
|
||||
let passes = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (passes++ === limit) break;
|
||||
sum += a[i] === undefined ? -1 : a[i];
|
||||
}
|
||||
return `${passes} ${sum}`;
|
||||
}
|
||||
|
||||
function partialDescending(a: number[], limit: number): string {
|
||||
let last = -1;
|
||||
let passes = 0;
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (passes++ === limit) break;
|
||||
last = i;
|
||||
}
|
||||
return `${passes} ${last}`;
|
||||
}
|
||||
|
||||
function stridedFromEnd(a: number[]): string {
|
||||
const hits: number[] = [];
|
||||
for (let i = a.length - 1; i >= 0 && hits.length < 3; i -= 1000000000) hits.push(i);
|
||||
return hits.join(",");
|
||||
}
|
||||
|
||||
function whileWithLimit(a: number[], limit: number): number {
|
||||
let i = 0;
|
||||
let present = 0;
|
||||
while (i < a.length && i < limit) {
|
||||
if (a[i] !== undefined) present++;
|
||||
i++;
|
||||
}
|
||||
return present * 1000 + i;
|
||||
}
|
||||
|
||||
function writeAtCounter(a: number[], limit: number): string {
|
||||
for (let i = 0; i < a.length && i < limit; i++) a[i] = i * 2;
|
||||
return `${a.length} ${a[0]} ${a[limit - 1]} ${a[limit]}`;
|
||||
}
|
||||
|
||||
function holeSum(a: number[]): string {
|
||||
let sum = 0;
|
||||
let nan = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const v = a[i];
|
||||
if (Number.isNaN(v + 0)) nan++;
|
||||
else sum += v;
|
||||
}
|
||||
return `${sum} ${nan}`;
|
||||
}
|
||||
|
||||
function firstValues(a: number[]): string {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (i === 4) break;
|
||||
s += a[i];
|
||||
}
|
||||
let t = 0;
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (i < a.length - 2) break;
|
||||
t += a[i];
|
||||
}
|
||||
let u = 0;
|
||||
for (let i = 0; i < a.length; i += 2) {
|
||||
if (i > 2) break;
|
||||
u += a[i];
|
||||
}
|
||||
return `${s} ${t} ${u}`;
|
||||
}
|
||||
|
||||
const huge: number[] = [1, 2, 3];
|
||||
huge.length = 3e9;
|
||||
console.log(huge.length);
|
||||
console.log(partialAscending(huge, 5));
|
||||
console.log(firstValues(huge), firstValues([1, 2, 3, 4, 5]));
|
||||
console.log(partialDescending(huge, 4));
|
||||
console.log(stridedFromEnd(huge));
|
||||
console.log(whileWithLimit(huge, 6));
|
||||
console.log(writeAtCounter(huge, 4));
|
||||
console.log(huge.length);
|
||||
|
||||
const edge: number[] = [];
|
||||
edge[4294967294] = 7;
|
||||
console.log(edge.length, partialDescending(edge, 2), partialAscending(edge, 3));
|
||||
|
||||
const holey = [1, 2];
|
||||
holey[3] = 4;
|
||||
holey[5] = 6;
|
||||
console.log(holeSum(holey));
|
||||
const sparse: number[] = [1, 2];
|
||||
sparse[10] = 5;
|
||||
console.log(holeSum(sparse));
|
||||
|
||||
// Becomes sparse mid-loop: a write far past the end through another binding.
|
||||
function sparseMidLoop(): string {
|
||||
const a = [1, 2, 3, 4];
|
||||
const b = a;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (i === 1) b[3000000000] = 9;
|
||||
if (i > 6) break;
|
||||
sum += a[i] === undefined ? 100 : a[i];
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
console.log(sparseMidLoop());
|
||||
|
||||
function holeMidLoop(): string {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (i === 1) a.length = 3;
|
||||
if (i === 2) a.length = 5;
|
||||
sum += a[i] === undefined ? 100 : a[i];
|
||||
}
|
||||
return `${sum}`;
|
||||
}
|
||||
console.log(holeMidLoop());
|
||||
|
||||
const objects: { v: number }[] = [{ v: 1 }, { v: 2 }, { v: 3 }];
|
||||
objects.length = 5;
|
||||
try {
|
||||
let total = 0;
|
||||
for (let i = 0; i < objects.length; i++) total += objects[i].v;
|
||||
console.log(total);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError ? "TypeError" : "other");
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Index loops whose array length the loop never changes, run over dense,
|
||||
// holey, partially filled, aliased and short arrays, with fractional,
|
||||
// negative and offset bounds, ascending, strided and descending counters.
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function sum(a: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) s += a[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
function scale(a: number[], b: number[]): string {
|
||||
for (let i = 0; i < a.length; i++) b[i] = a[i] * 2;
|
||||
return `${b.length}:${b.join(",")}`;
|
||||
}
|
||||
|
||||
function inPlace(a: number[]): string {
|
||||
for (let i = 0; i < a.length; i++) a[i] = a[i] + 1;
|
||||
return a.join(",");
|
||||
}
|
||||
|
||||
function strided(a: number[], step: number): string {
|
||||
let s = 0;
|
||||
if (step === 2) for (let i = 0; i < a.length; i += 2) s += a[i];
|
||||
else for (let i = 1; i < a.length; i += 3) s += a[i];
|
||||
return `${s}`;
|
||||
}
|
||||
|
||||
function descending(a: number[]): string {
|
||||
const out: number[] = [];
|
||||
for (let i = a.length - 1; i >= 0; i--) out.push(a[i]);
|
||||
return out.join(",");
|
||||
}
|
||||
|
||||
function descendingPairs(a: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = a.length - 2; i > 0; i -= 2) s += a[i] - a[i + 1];
|
||||
return s;
|
||||
}
|
||||
|
||||
function offsetBounds(a: number[]): string {
|
||||
let inner = 0;
|
||||
for (let i = 1; i < a.length - 1; i++) inner += a[i];
|
||||
let inclusive = 0;
|
||||
for (let i = 0; i <= a.length - 1; i++) inclusive += a[i];
|
||||
let diffs = 0;
|
||||
for (let i = 0; i < a.length - 1; i++) diffs += a[i + 1] - a[i];
|
||||
return `${inner} ${inclusive} ${diffs}`;
|
||||
}
|
||||
|
||||
function fractionalBounds(a: number[]): string {
|
||||
const seen: number[] = [];
|
||||
for (let i = 0.5; i < a.length; i++) seen.push(a[i] === undefined ? -1 : 1);
|
||||
for (let i = -2; i < a.length - 3; i++) seen.push(a[i] === undefined ? -2 : a[i]);
|
||||
for (let i = 0; i < a.length - 0.5; i++) seen.push(i);
|
||||
return seen.join(",");
|
||||
}
|
||||
|
||||
function whileLoop(a: number[]): number {
|
||||
let i = 0;
|
||||
let s = 0;
|
||||
while (i < a.length) {
|
||||
s += a[i] * i;
|
||||
i++;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function constLength(a: number[]): number {
|
||||
const n = a.length;
|
||||
let s = 0;
|
||||
for (let i = 0; i < n; i++) s += a[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
function points(ps: Point[]): string {
|
||||
let s = 0;
|
||||
for (let i = 0; i < ps.length; i++) s += ps[i].x * ps[i].y;
|
||||
return `${s}`;
|
||||
}
|
||||
|
||||
function flags(bs: boolean[]): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < bs.length; i++) if (bs[i]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function names(ss: string[]): string {
|
||||
let out = "";
|
||||
for (let i = ss.length - 1; i >= 0; i--) out += ss[i];
|
||||
return out;
|
||||
}
|
||||
|
||||
function maybe(xs: (number | undefined)[]): string {
|
||||
let s = 0;
|
||||
let missing = 0;
|
||||
for (let i = 0; i < xs.length; i++) {
|
||||
const v = xs[i];
|
||||
if (v === undefined) missing++;
|
||||
else s += v;
|
||||
}
|
||||
return `${s} ${missing}`;
|
||||
}
|
||||
|
||||
function nested(g: number[][]): number {
|
||||
let s = 0;
|
||||
for (let r = 0; r < g.length; r++) {
|
||||
const row = g[r];
|
||||
for (let c = 0; c < row.length; c++) s += row[c] * (r + 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function counterAsNumber(a: number[]): string {
|
||||
let mixed = 0;
|
||||
for (let i = 0; i < a.length; i++) mixed += i / 2 + (i | 0) + a[i] * -0;
|
||||
return `${mixed} ${Object.is(mixed, -0)}`;
|
||||
}
|
||||
|
||||
function earlyExit(a: number[], target: number): number {
|
||||
for (let i = 0; i < a.length; i++) if (a[i] === target) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const dense = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
console.log(sum(dense), sum([]), sum([0.5, -0.25]));
|
||||
console.log(scale(dense, new Array<number>(8).fill(0)));
|
||||
console.log(scale(dense, [9, 9]));
|
||||
console.log(scale([1, 2, 3], []));
|
||||
const self = [1, 2, 3, 4];
|
||||
console.log(scale(self, self));
|
||||
console.log(inPlace([10, 20, 30]));
|
||||
console.log(strided(dense, 2), strided(dense, 3));
|
||||
console.log(descending(dense), descending([]));
|
||||
console.log(descendingPairs(dense), descendingPairs([1]));
|
||||
console.log(offsetBounds(dense), offsetBounds([4]), offsetBounds([]));
|
||||
console.log(fractionalBounds([1, 2, 3, 4, 5]));
|
||||
console.log(whileLoop(dense));
|
||||
console.log(constLength(dense));
|
||||
console.log(points([{ x: 1, y: 2 }, { x: 3, y: 4 }]));
|
||||
console.log(flags([true, false, true, true]));
|
||||
console.log(names(["a", "b", "c"]));
|
||||
console.log(maybe([1, undefined, 3]));
|
||||
console.log(nested([[1, 2], [3], [], [4, 5, 6]]));
|
||||
console.log(counterAsNumber([1, 2, 3]));
|
||||
console.log(earlyExit(dense, 5), earlyExit(dense, 50));
|
||||
|
||||
// Holes and partially filled storage take the checked path.
|
||||
const holey = [1];
|
||||
holey[2] = 3;
|
||||
holey[4] = 5;
|
||||
console.log(sum(holey), descending(holey), whileLoop(holey));
|
||||
const target = [0, 0, 0];
|
||||
target[4] = 0;
|
||||
console.log(scale(dense.slice(0, 5), target));
|
||||
const gap: number[] = [];
|
||||
gap[3] = 1;
|
||||
console.log(sum(gap), scale([1, 2, 3, 4], gap));
|
||||
const shortened = [1, 2, 3, 4, 5, 6];
|
||||
shortened.length = 3;
|
||||
shortened.length = 6;
|
||||
console.log(sum(shortened), constLength(shortened));
|
||||
const holeyPoints: Point[] = [{ x: 1, y: 1 }];
|
||||
holeyPoints[2] = { x: 2, y: 2 };
|
||||
try {
|
||||
console.log(points(holeyPoints));
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError ? "TypeError" : "other");
|
||||
}
|
||||
Reference in New Issue
Block a user