Start integer loops from the integers their starts carry

This commit is contained in:
Chris Tate
2026-10-24 22:16:05 -05:00
parent cf399bc1bf
commit fe8ba8900f
7 changed files with 121 additions and 15 deletions
@@ -701,7 +701,8 @@ test("a two-counter loop versions the typed arrays it indexes at either counter"
};
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
expect(guard).toMatch(/icmp sge i64 %\S+, 0/);
// `i` starts at the literal 0, so its test compares the integer itself.
expect(guard).toMatch(/icmp sge i64 0, 0/);
expect(guard).toMatch(/icmp slt i64 %\S+, %\S+/);
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
// The fast copy keeps only the `src[i + 1]` check, and no second guard.
@@ -9,6 +9,7 @@ import {
emitCountedLoopLimit,
emitCountedLoopSafety,
emitExactIntegerTest,
emitStartIntegerTest,
} from "./counted-loops.js";
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
import { f64Lit } from "./common.js";
@@ -1251,8 +1252,8 @@ export function emitBytesLoopVersionGuard(
// access at either one is in bounds when `lo >= 0` and `hi < length`.
const { loop } = version.paired;
const [low, high] = [loop.low.start, loop.high.start].map((expr) => {
const tested = emitExactIntegerTest(host, host.emitExpr(expr).name, -SAFE, SAFE);
conditions.push(tested.ok);
const tested = emitStartIntegerTest(host, expr, -SAFE, SAFE);
if (tested.ok !== "true") conditions.push(tested.ok);
return tested.integer;
});
const nonNegative = B.tmp();
@@ -7,6 +7,7 @@ import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js";
import type { IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
/** The caller has proved or guarded the direction's finite endpoint. Clamp
* the opposite infinity before integer conversion. Fractional limits become
@@ -121,6 +122,7 @@ export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPair
}
function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string {
if (ok === "true") return safe;
if (safe === "true") return ok;
const both = host.B.tmp();
host.B.line(`${both} = and i1 ${safe}, ${ok}`);
@@ -129,10 +131,25 @@ function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string {
/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit
* patterns of the value and its round trip agree). NaN and infinities fail
* the ordered range tests. */
* the ordered range tests. A start with an integer form (an int32 local,
* `p0 - 1` of one) is an exact integer already: its range answers the test,
* or two integer compares do. */
function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string {
const B = host.B;
const start = host.emitExpr(expr);
const integer = exactInteger(host, start, expr);
if (integer) {
const limit = Number.MAX_SAFE_INTEGER;
if (integer.range.min >= -limit && integer.range.max <= limit) return "true";
const wide = widenInteger(host, integer),
low = B.tmp(),
high = B.tmp(),
ok = B.tmp();
B.line(`${low} = icmp sge i64 ${wide}, ${-limit}`);
B.line(`${high} = icmp sle i64 ${wide}, ${limit}`);
B.line(`${ok} = and i1 ${low}, ${high}`);
return ok;
}
const low = B.tmp(),
high = B.tmp(),
raw = B.tmp(),
@@ -188,3 +205,39 @@ export function emitExactIntegerTest(
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
return { ok, integer };
}
/** The i64 start of an integer counter: the integer form the start value
* carries (an int32 local, `p0 - 1` of one), or the double's conversion,
* which the loop's guard or proof makes exact. */
export function emitCountedStart(host: LlvmEmitterContext, expr: IrExpr): string {
const value = host.emitExpr(expr);
const integer = exactInteger(host, value, expr);
if (integer) return widenInteger(host, integer);
const start = host.B.tmp();
host.B.line(`${start} = fptosi double ${value.name} to i64`);
return start;
}
/** emitExactIntegerTest of a start expression: when the value carries an
* integer form (an exact integer, never -0), its range answers the test or
* two integer compares do, and the integer is the form's. */
export function emitStartIntegerTest(
host: LlvmEmitterContext,
expr: IrExpr,
min: number,
max: number,
): { ok: string; integer: string } {
const value = host.emitExpr(expr);
const integer = exactInteger(host, value, expr);
if (!integer) return emitExactIntegerTest(host, value.name, min, max);
const wide = widenInteger(host, integer);
if (integer.range.min >= min && integer.range.max <= max) return { ok: "true", integer: wide };
const B = host.B;
const low = B.tmp(),
high = B.tmp(),
ok = B.tmp();
B.line(`${low} = icmp sge i64 ${wide}, ${min}`);
B.line(`${high} = icmp sle i64 ${wide}, ${max}`);
B.line(`${ok} = and i1 ${low}, ${high}`);
return { ok, integer: wide };
}
@@ -36,7 +36,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
import { emitCountedLoopLimit, emitExactIntegerTest } from "./counted-loops.js";
import { emitCountedLoopLimit, emitStartIntegerTest } from "./counted-loops.js";
export const INT32_MAX = 2147483647;
@@ -453,7 +453,7 @@ function emitPairedDenseGuard(
): string {
const B = host.B;
const start = (expr: IrExpr, min: number, max: number) =>
emitExactIntegerTest(host, host.emitExpr(expr).name, min, max);
emitStartIntegerTest(host, expr, min, max);
const low = start(pair.lowStart, 0, INT32_MAX - 1);
const high = start(pair.highStart, -1, INT32_MAX - 1);
let valid = B.tmp();
@@ -61,6 +61,7 @@ import {
import {
emitCountedLoopLimit,
emitCountedLoopSafety,
emitCountedStart,
emitPairedLoopSafety,
} from "./counted-loops.js";
import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js";
@@ -1112,9 +1113,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.entryAllocas.push(
`${integerSlot} = alloca i64 ; integer induction ${emitter.currentLocals.get(countedLoop.localId)!.name}`,
);
const startNumber = emitter.emitExpr(countedLoop.start);
const start = B.tmp();
B.line(`${start} = fptosi double ${startNumber.name} to i64`);
const start = emitCountedStart(emitter, countedLoop.start);
B.line(`store i64 ${start}, ptr ${integerSlot}`);
emitter.integerLoopBindings.set(countedLoop.localId, {
slot: integerSlot,
@@ -1130,9 +1129,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.entryAllocas.push(
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`,
);
const value = emitter.emitExpr(follower.start);
const followerStart = B.tmp();
B.line(`${followerStart} = fptosi double ${value.name} to i64`);
const followerStart = emitCountedStart(emitter, follower.start);
B.line(`store i64 ${followerStart}, ptr ${slot}`);
followerSlots.push(slot);
emitter.integerLoopBindings.set(follower.localId, {
@@ -1149,9 +1146,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.entryAllocas.push(
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`,
);
const value = emitter.emitExpr(counter.start);
const start = B.tmp();
B.line(`${start} = fptosi double ${value.name} to i64`);
const start = emitCountedStart(emitter, counter.start);
B.line(`store i64 ${start}, ptr ${slot}`);
pairedSlots.push(slot);
const dense = LOOPS.of(emitter).denseLoop;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8196-counted-loops-integer-starts.ts"
],
"diags": []
}
@@ -0,0 +1,50 @@
// Counted, paired and dense array loops whose start is an int32 value
// (an int32 array element, `p0 | 0` or one minus either): starts at -1, 0,
// inside the array, at its end, and at the int32 extremes, where the loop
// must not run or must stop at the array's end as Node does. Number arrays
// and an Int32Array both run their paired loops.
function reverse(pp: number[], p0: number): number {
let swaps = 0;
for (let i = 1, j = (p0 | 0) - 1; i < j; i++, j--) {
const t = pp[i]!;
pp[i] = pp[j]!;
pp[j] = t;
swaps++;
}
return swaps;
}
function reverseBytes(values: Int32Array, p0: number): number {
let swaps = 0;
for (let i = 0, j = (p0 | 0) - 1; i < j; i++, j--) {
const t = values[i]!;
values[i] = values[j]!;
values[j] = t;
swaps++;
}
return swaps;
}
function sumFrom(values: number[], start: number): number {
let total = 0;
for (let k = start - 1; k < values.length; k++) total += values[k] ?? 100;
return total;
}
function countdown(values: Int32Array, from: number): number {
let total = 0;
for (let k = from; k >= 0; k--) total += values[k] ?? 1000;
return total;
}
const starts: number[] = [0, 1, 2, 5, 9, 10, 11, 2147483647, -2147483648];
const ints = new Int32Array(starts);
for (let s = 0; s < ints.length; s++) {
const p0 = ints[s]!;
const pp = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const swaps = p0 <= 11 ? reverse(pp, p0) : 0;
const bytes = new Int32Array(pp);
const byteSwaps = p0 <= 11 ? reverseBytes(bytes, p0) : 0;
console.log(p0, swaps, pp.join(""), p0 <= 11 && p0 >= -1 ? sumFrom(pp, p0) : "-", p0 <= 11 ? countdown(ints, p0) : "-");
console.log(" bytes", byteSwaps, bytes.join(""));
}