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:
Malte Ubl
2026-10-17 14:48:38 -07:00
co-authored by Claude Opus 5.5
parent 6ce668b213
commit 714ef5aa09
16 changed files with 1599 additions and 29 deletions
@@ -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");
}