mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Run counters that step beside a counted loop's counter as integers (#1829)
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type {
|
||||
IntegerCountedForLoop,
|
||||
IntegerLoopFollower,
|
||||
IntegerPairedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
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";
|
||||
@@ -61,9 +66,47 @@ export function emitCountedLoopSafety(
|
||||
);
|
||||
}
|
||||
if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start));
|
||||
for (const follower of loop.followers) {
|
||||
if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start));
|
||||
if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower));
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
/** `|start| + |step| * span / |driver step| <= 2^52` for a follower, where
|
||||
* `span` is the distance from the driver's start to the limit. The body runs
|
||||
* at most `span / |driver step| + 1` times, so every value the follower holds
|
||||
* on a body entry is within the bound. NaN and infinities fail the ordered
|
||||
* test. */
|
||||
function emitFollowerReach(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
follower: IntegerLoopFollower,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const start = host.emitExpr(loop.start),
|
||||
limit = host.emitExpr(loop.limit),
|
||||
from = host.emitExpr(follower.start);
|
||||
const span = B.tmp(),
|
||||
reach = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
total = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(
|
||||
loop.step > 0
|
||||
? `${span} = fsub double ${limit.name}, ${start.name}`
|
||||
: `${span} = fsub double ${start.name}, ${limit.name}`,
|
||||
);
|
||||
B.line(
|
||||
`${reach} = fmul double ${span}, ${f64Lit(Math.abs(follower.step) / Math.abs(loop.step))}`,
|
||||
);
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${from.name})`);
|
||||
B.line(`${total} = fadd double ${magnitude}, ${reach}`);
|
||||
B.line(`${ok} = fcmp ole double ${total}, ${f64Lit(FOLLOWER_GUARD_LIMIT)}`);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** The run-time test of a guarded paired loop: each unproven start is an
|
||||
* exact safe integer. */
|
||||
export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string {
|
||||
|
||||
@@ -120,6 +120,7 @@ const counted = (
|
||||
limitGuarded: false,
|
||||
startGuarded: false,
|
||||
guardLimit: 0,
|
||||
followers: [],
|
||||
...fields,
|
||||
});
|
||||
|
||||
|
||||
@@ -900,8 +900,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
!integerLoop && !countedLoop && emitter.countedLoopsEnabled
|
||||
? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges)
|
||||
: null;
|
||||
// Byte windows and dense loops reason about one counter.
|
||||
const window =
|
||||
countedLoop && emitter.byteWindowsEnabled
|
||||
countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled
|
||||
? matchByteWindow(
|
||||
s,
|
||||
countedLoop,
|
||||
@@ -919,7 +920,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
|
||||
range: { min: 0, max: 4294967294 },
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded
|
||||
: countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
@@ -1051,6 +1052,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
let integerSlot: string | null = null;
|
||||
let countedLimit: string | null = null;
|
||||
const pairedSlots: string[] = [];
|
||||
const followerSlots: string[] = [];
|
||||
if (integerLoop) {
|
||||
integerSlot = B.slot();
|
||||
B.entryAllocas.push(
|
||||
@@ -1094,6 +1096,23 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
? emitter.denseLoop.range
|
||||
: countedLoop.range,
|
||||
});
|
||||
for (const follower of countedLoop.followers) {
|
||||
const slot = B.slot();
|
||||
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`);
|
||||
B.line(`store i64 ${followerStart}, ptr ${slot}`);
|
||||
followerSlots.push(slot);
|
||||
emitter.integerLoopBindings.set(follower.localId, {
|
||||
slot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: follower.range,
|
||||
});
|
||||
}
|
||||
countedLimit = emitCountedLoopLimit(emitter, countedLoop);
|
||||
} else if (pairedLoop) {
|
||||
for (const counter of [pairedLoop.low, pairedLoop.high]) {
|
||||
@@ -1234,6 +1253,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`${old} = load i64, ptr ${integerSlot}`);
|
||||
B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`);
|
||||
B.line(`store i64 ${next}, ptr ${integerSlot}`);
|
||||
countedLoop.followers.forEach((follower, i) => {
|
||||
const was = B.tmp();
|
||||
const now = B.tmp();
|
||||
B.line(`${was} = load i64, ptr ${followerSlots[i]}`);
|
||||
B.line(`${now} = add nsw i64 ${was}, ${follower.step}`);
|
||||
B.line(`store i64 ${now}, ptr ${followerSlots[i]}`);
|
||||
});
|
||||
} else if (pairedLoop) {
|
||||
[pairedLoop.low, pairedLoop.high].forEach((counter, i) => {
|
||||
const old = B.tmp();
|
||||
@@ -1249,7 +1275,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.startBlock(le);
|
||||
emitter.releaseScope(emitter.scopes.pop()!);
|
||||
if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId);
|
||||
if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId);
|
||||
if (countedLoop) {
|
||||
emitter.integerLoopBindings.delete(countedLoop.localId);
|
||||
for (const follower of countedLoop.followers)
|
||||
emitter.integerLoopBindings.delete(follower.localId);
|
||||
}
|
||||
if (pairedLoop) {
|
||||
emitter.integerLoopBindings.delete(pairedLoop.low.localId);
|
||||
emitter.integerLoopBindings.delete(pairedLoop.high.localId);
|
||||
|
||||
@@ -426,3 +426,119 @@ test("paired counters closing in on each other run as integers", () => {
|
||||
unused.body = [];
|
||||
expect(matchIntegerPairedForLoop(unused, vars)).toBeNull();
|
||||
});
|
||||
|
||||
test("counters stepped beside the tested one run as integers", () => {
|
||||
const vars = new Map(locals);
|
||||
for (const id of ["j", "k", "limit", "from"])
|
||||
vars.set(id, { id, name: id, type: F64, mutable: true });
|
||||
vars.set("s", { id: "s", name: "s", type: STRING, mutable: false });
|
||||
const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: id,
|
||||
value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const read = (index: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver: { kind: "varRef", localId: "s", type: STRING, loc },
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const comoving = (
|
||||
limit: IrExpr,
|
||||
followerStart: IrExpr = number(0),
|
||||
update: IrStmt = { kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc },
|
||||
) =>
|
||||
({
|
||||
kind: "for",
|
||||
loc,
|
||||
init: {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: "i", init: number(0), loc },
|
||||
{ kind: "varDecl", localId: "j", init: followerStart, loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
cond: { kind: "bin", op: "<", left: ref("i"), right: limit, type: BOOL, loc },
|
||||
update,
|
||||
body: [read(ref("j"))],
|
||||
}) as IrStmt & { kind: "for" };
|
||||
// A literal limit bounds the follower: at most 9 updates of 4 before the
|
||||
// last body entry.
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10)), vars)).toMatchObject({
|
||||
localId: "i",
|
||||
guarded: false,
|
||||
range: { min: 0, max: 9 },
|
||||
followers: [
|
||||
{
|
||||
localId: "j",
|
||||
step: 4,
|
||||
startGuarded: false,
|
||||
tripGuarded: false,
|
||||
range: { min: 0, max: 36 },
|
||||
},
|
||||
],
|
||||
});
|
||||
// An unknown limit or start is tested at run time.
|
||||
expect(matchIntegerCountedForLoop(comoving(ref("limit")), vars)).toMatchObject({
|
||||
guarded: true,
|
||||
followers: [{ tripGuarded: true, range: { min: 0, max: 2 ** 52 + 2 } }],
|
||||
});
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10), ref("from")), vars)).toMatchObject({
|
||||
guarded: true,
|
||||
followers: [{ startGuarded: true, tripGuarded: true }],
|
||||
});
|
||||
// A literal limit with known starts that can pass 2^53 is refused.
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10), number(2 ** 53 - 9)), vars)).toBeNull();
|
||||
// Nested update blocks (`i++, j += 4, k--`) step three counters.
|
||||
const three = comoving(number(10), number(0), {
|
||||
kind: "block",
|
||||
body: [{ kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc }, step("k", "-")],
|
||||
loc,
|
||||
});
|
||||
if (three.init?.kind === "block")
|
||||
three.init.body.push({ kind: "varDecl", localId: "k", init: number(5), loc });
|
||||
expect(matchIntegerCountedForLoop(three, vars)?.followers).toMatchObject([
|
||||
{ localId: "j", range: { min: 0, max: 36 } },
|
||||
{ localId: "k", step: -1, range: { min: -4, max: 5 } },
|
||||
]);
|
||||
// A follower without a step, one the body writes, one the limit reads, a
|
||||
// captured one, and a start that reads another counter are refused.
|
||||
expect(
|
||||
matchIntegerCountedForLoop(
|
||||
comoving(number(10), number(0), { kind: "block", body: [step("i", "+")], loc }),
|
||||
vars,
|
||||
),
|
||||
).toBeNull();
|
||||
const writing = comoving(number(10));
|
||||
writing.body.push(step("j", "+"));
|
||||
expect(matchIntegerCountedForLoop(writing, vars)).toBeNull();
|
||||
expect(matchIntegerCountedForLoop(comoving(ref("j")), vars)).toBeNull();
|
||||
const boxed = new Map(vars);
|
||||
boxed.set("j", { ...vars.get("j")!, boxed: true });
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10)), boxed)).toBeNull();
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10), ref("i")), vars)).toBeNull();
|
||||
// A driver span above 2^53 - 1 bounds no trip count. From -(2^53 - 1) to
|
||||
// below 2^52 - 1 the span 3 * 2^52 - 3 rounds down to 3 * 2^52 - 4, which
|
||||
// would put this follower's last value at 2^53 - 1 instead of 2^53.
|
||||
const wide = comoving(number(2 ** 52 - 1), number(-(2 ** 52) + 3), {
|
||||
kind: "block",
|
||||
body: [step("i", "+"), step("j", "+")],
|
||||
loc,
|
||||
});
|
||||
if (wide.init?.kind === "block" && wide.init.body[0]?.kind === "varDecl")
|
||||
wide.init.body[0].init = number(-Number.MAX_SAFE_INTEGER);
|
||||
expect(matchIntegerCountedForLoop(wide, vars)).toBeNull();
|
||||
if (wide.init?.kind === "block" && wide.init.body[1]?.kind === "varDecl")
|
||||
wide.init.body[1].init = ref("from");
|
||||
expect(matchIntegerCountedForLoop(wide, vars)?.followers).toMatchObject([
|
||||
{ startGuarded: true, tripGuarded: true },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,8 +28,36 @@ export interface IntegerCountedForLoop {
|
||||
startGuarded: boolean;
|
||||
guardLimit: number;
|
||||
range: IntegerRange;
|
||||
/** Counters declared and stepped beside this one that the condition does
|
||||
* not test, such as `j` in `for (let i = 0, j = 0; i < n; i++, j += 4)`. */
|
||||
followers: IntegerLoopFollower[];
|
||||
}
|
||||
|
||||
/** A counter that steps by a constant on every update of a counted loop.
|
||||
* After `k` updates it holds `start + step * k`. */
|
||||
export interface IntegerLoopFollower {
|
||||
localId: string;
|
||||
start: IrExpr;
|
||||
step: number;
|
||||
/** As the driver's `startGuarded` (part of the loop's `guarded`). */
|
||||
startGuarded: boolean;
|
||||
/** The follower's range depends on the run-time trip count: the versioned
|
||||
* loop also tests `|start| + |step| * span / |driver step|` against
|
||||
* `FOLLOWER_GUARD_LIMIT`, where `span` is the distance from the driver's
|
||||
* start to the limit (part of the loop's `guarded`). */
|
||||
tripGuarded: boolean;
|
||||
/** The follower's values on every body entry. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** The bound of the run-time follower test. */
|
||||
export const FOLLOWER_GUARD_LIMIT = 2 ** 52;
|
||||
/** The magnitude a follower that passed the test can reach. The test's four
|
||||
* f64 operations (the step ratio, the span, its product and the sum) each
|
||||
* round down by at most a factor of 1 - 2^-53, so the exact sum is below
|
||||
* `2^52 / (1 - 2^-53)^4`, which is less than 2^52 + 3. */
|
||||
const FOLLOWER_GUARD_RANGE = FOLLOWER_GUARD_LIMIT + 2;
|
||||
|
||||
function constantStep(update: IrStmt | null, id: string): number | null {
|
||||
if (update?.kind === "exprStmt" && update.expr.kind === "incDec" && update.expr.localId === id)
|
||||
return update.expr.op === "+" ? 1 : -1;
|
||||
@@ -115,33 +143,61 @@ export function matchIntegerCountedForLoop(
|
||||
ranges: IntegerRanges = new Map(),
|
||||
unaliased: ReadonlySet<string> | null = null,
|
||||
): IntegerCountedForLoop | null {
|
||||
const init = stmt.init;
|
||||
if (init?.kind !== "varDecl" || !init.init) return null;
|
||||
const local = locals.get(init.localId);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
const start = init.init;
|
||||
let startRange =
|
||||
start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0)
|
||||
? { min: start.value, max: start.value }
|
||||
: ranges.get(start);
|
||||
// An unproven start is tested at run time, on a versioned loop.
|
||||
const startGuarded = !startRange && start.kind !== "numLit" && start.type.kind === "f64";
|
||||
if (startGuarded) startRange = { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
|
||||
if (
|
||||
!startRange ||
|
||||
startRange.min < -Number.MAX_SAFE_INTEGER ||
|
||||
startRange.max > Number.MAX_SAFE_INTEGER ||
|
||||
!stableLoopValue(start, locals, stmt.body, false, local.id)
|
||||
)
|
||||
return null;
|
||||
const step = constantStep(stmt.update, local.id);
|
||||
if (step === null || writesLocal(stmt.body, local.id)) return null;
|
||||
const cond = stmt.cond;
|
||||
if (cond?.kind !== "bin") return null;
|
||||
const decls = stmt.init?.kind === "block" ? stmt.init.body : stmt.init ? [stmt.init] : [];
|
||||
// `i++, j += 2, k--` lowers to nested blocks of steps.
|
||||
const flat = (u: IrStmt | null): (IrStmt | null)[] =>
|
||||
u?.kind === "block" ? u.body.flatMap(flat) : [u];
|
||||
const updates = flat(stmt.update);
|
||||
if (decls.length === 0 || updates.length !== decls.length) return null;
|
||||
const declared = (e: IrExpr): string | null =>
|
||||
e.kind === "varRef" && decls.some((d) => d.kind === "varDecl" && d.localId === e.localId)
|
||||
? e.localId
|
||||
: null;
|
||||
// The condition tests the driving counter; every other declaration
|
||||
// follows it.
|
||||
const driver = declared(cond.left) ?? declared(cond.right);
|
||||
const counterIds = decls.map((d) => (d.kind === "varDecl" ? d.localId : ""));
|
||||
const stepOf = (id: string): number | null => {
|
||||
const steps = updates.map((u) => constantStep(u, id)).filter((v) => v !== null);
|
||||
return steps.length === 1 ? steps[0]! : null;
|
||||
};
|
||||
// A counter's start is a stable integer that refers to no counter. An
|
||||
// unproven start is tested at run time, on a versioned loop.
|
||||
const counterStart = (
|
||||
id: string,
|
||||
): { start: IrExpr; range: IntegerRange; guarded: boolean; step: number } | null => {
|
||||
const decl = decls.find((d) => d.kind === "varDecl" && d.localId === id);
|
||||
if (decl?.kind !== "varDecl" || !decl.init) return null;
|
||||
const local = locals.get(id);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
const start = decl.init;
|
||||
const known =
|
||||
start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0)
|
||||
? { min: start.value, max: start.value }
|
||||
: ranges.get(start);
|
||||
const guarded = !known && start.kind !== "numLit" && start.type.kind === "f64";
|
||||
const range = guarded ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } : known;
|
||||
const step = stepOf(id);
|
||||
if (
|
||||
!range ||
|
||||
range.min < -Number.MAX_SAFE_INTEGER ||
|
||||
range.max > Number.MAX_SAFE_INTEGER ||
|
||||
!counterIds.every((counter) => stableLoopValue(start, locals, stmt.body, false, counter)) ||
|
||||
step === null ||
|
||||
writesLocal(stmt.body, id)
|
||||
)
|
||||
return null;
|
||||
return { start, range, guarded, step };
|
||||
};
|
||||
const head = driver === null ? null : counterStart(driver);
|
||||
if (!head) return null;
|
||||
const local = locals.get(driver!)!;
|
||||
const { start, step, guarded: startGuarded, range: startRange } = head;
|
||||
let limit = cond.right,
|
||||
op: string = cond.op;
|
||||
if (cond.left.kind !== "varRef" || cond.left.localId !== local.id) {
|
||||
if (cond.right.kind !== "varRef" || cond.right.localId !== local.id) return null;
|
||||
limit = cond.left;
|
||||
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
|
||||
}
|
||||
@@ -185,11 +241,70 @@ export function matchIntegerCountedForLoop(
|
||||
const limitGuarded =
|
||||
!boundRange || (step > 0 ? !(boundRange.max <= guardLimit) : !(boundRange.min >= guardLimit));
|
||||
if (limitGuarded && limit.kind === "numLit") return null;
|
||||
const guarded = limitGuarded || startGuarded;
|
||||
const range =
|
||||
step > 0
|
||||
? {
|
||||
min: startRange.min,
|
||||
max: Math.max(
|
||||
startRange.min,
|
||||
Math.min(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.ceil(boundRange.max) - (inclusive ? 0 : 1)
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
}
|
||||
: {
|
||||
min: Math.min(
|
||||
startRange.max,
|
||||
Math.max(
|
||||
-Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.floor(boundRange.min) + (inclusive ? 0 : 1)
|
||||
: -Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
max: startRange.max,
|
||||
};
|
||||
// On body entry k the driver holds `start + step * k`, so k is at most the
|
||||
// driver's span over |step|, and a follower holds its start plus its own
|
||||
// step times k. A span above 2^53 - 1 can round down, so it bounds no
|
||||
// trip count. Below it the quotient rounds to at least its floor, and the
|
||||
// follower's products and sums below are exact while they stay within
|
||||
// ±(2^53 - 1) and round outside it otherwise.
|
||||
const span = step > 0 ? range.max - startRange.min : startRange.max - range.min;
|
||||
const trips = span <= Number.MAX_SAFE_INTEGER ? Math.floor(span / Math.abs(step)) : Infinity;
|
||||
const followers: IntegerLoopFollower[] = [];
|
||||
for (const id of counterIds) {
|
||||
if (id === local.id) continue;
|
||||
const follower = counterStart(id);
|
||||
// The limit is evaluated once; a follower changes on every update.
|
||||
if (!follower || !stableLoopValue(limit, locals, stmt.body, false, id)) return null;
|
||||
const reach = follower.step * trips;
|
||||
const min = follower.range.min + Math.min(0, reach),
|
||||
max = follower.range.max + Math.max(0, reach);
|
||||
const tripGuarded = min < -Number.MAX_SAFE_INTEGER || max > Number.MAX_SAFE_INTEGER;
|
||||
// Known starts and a literal limit are not tested at run time.
|
||||
if (tripGuarded && !follower.guarded && !startGuarded && limit.kind === "numLit") return null;
|
||||
followers.push({
|
||||
localId: id,
|
||||
start: follower.start,
|
||||
step: follower.step,
|
||||
startGuarded: follower.guarded,
|
||||
tripGuarded,
|
||||
range: tripGuarded
|
||||
? { min: Math.max(-FOLLOWER_GUARD_RANGE, min), max: Math.min(FOLLOWER_GUARD_RANGE, max) }
|
||||
: { min, max },
|
||||
});
|
||||
}
|
||||
const guarded =
|
||||
limitGuarded || startGuarded || followers.some((f) => f.startGuarded || f.tripGuarded);
|
||||
let useful = false;
|
||||
let cost = 0;
|
||||
const counter = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && e.localId === local.id) ||
|
||||
(e.kind === "varRef" &&
|
||||
(e.localId === local.id || followers.some((f) => f.localId === e.localId))) ||
|
||||
(e.kind === "bin" &&
|
||||
(e.op === "+" || e.op === "-" || e.op === "*") &&
|
||||
(counter(e.left) || counter(e.right)));
|
||||
@@ -241,32 +356,6 @@ export function matchIntegerCountedForLoop(
|
||||
return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt);
|
||||
};
|
||||
if (!stmt.body.every(bodyStmt) || !useful) return null;
|
||||
const range =
|
||||
step > 0
|
||||
? {
|
||||
min: startRange.min,
|
||||
max: Math.max(
|
||||
startRange.min,
|
||||
Math.min(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.ceil(boundRange.max) - (inclusive ? 0 : 1)
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
}
|
||||
: {
|
||||
min: Math.min(
|
||||
startRange.max,
|
||||
Math.max(
|
||||
-Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.floor(boundRange.min) + (inclusive ? 0 : 1)
|
||||
: -Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
max: startRange.max,
|
||||
};
|
||||
return {
|
||||
localId: local.id,
|
||||
start,
|
||||
@@ -278,6 +367,7 @@ export function matchIntegerCountedForLoop(
|
||||
startGuarded,
|
||||
guardLimit,
|
||||
range,
|
||||
followers,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7141-comoving-loop-counters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// For loops with a second counter that steps beside the tested one
|
||||
// (`i++, j += 4`): pixel strides over typed arrays, interleaving into number
|
||||
// arrays, descending and inclusive loops, three counters, early exits,
|
||||
// starts and limits that are fractions, negative zero, NaN, infinite or
|
||||
// near 2^53, counters that live across a yield or an await, and counters a
|
||||
// closure captures, the body writes or the condition reads.
|
||||
function luminance(px: Uint8Array, count: number): number[] {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, p = 0; i < count; i++, p += 4) {
|
||||
out.push((px[p]! * 77 + px[p + 1]! * 150 + px[p + 2]! * 29) >> 8);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const px = new Uint8Array(32);
|
||||
for (let k = 0; k < px.length; k++) px[k] = (k * 37 + 11) & 255;
|
||||
console.log(luminance(px, 8).join(","));
|
||||
// Past the end, reads are undefined and the sums NaN, which `>> 8` makes 0.
|
||||
console.log(luminance(px, 10).join(","));
|
||||
console.log(luminance(px, 0).length, luminance(px, -3).length, luminance(px, 2.5).join(","));
|
||||
console.log(luminance(px, NaN).length);
|
||||
|
||||
function interleave(a: number[], b: number[]): number[] {
|
||||
const out: number[] = new Array(a.length * 2).fill(-1);
|
||||
const n = a.length;
|
||||
for (let i = 0, j = 0; i < n; i++, j += 2) {
|
||||
out[j] = a[i]!;
|
||||
out[j + 1] = b[i]!;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
console.log(interleave([1, 2, 3, 4], [10, 20, 30, 40]).join(","));
|
||||
console.log(interleave([], []).length);
|
||||
|
||||
// The second counter's start is not proven: charCodeAt truncates its index,
|
||||
// and the counter's own values keep JS semantics.
|
||||
function codes(s: string, count: number, from: number, step: number): string {
|
||||
const out: (number | string)[] = [];
|
||||
if (step === 3) {
|
||||
for (let i = 0, j = from; i < count; i++, j += 3) out.push(s.charCodeAt(j), j);
|
||||
} else {
|
||||
for (let i = 0, j = from; i < count; i++, j -= 2) out.push(s.charCodeAt(j), j);
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
const word = "abcdefghijklmnop";
|
||||
console.log(codes(word, 4, 0, 3), "|", codes(word, 4, 15, -2));
|
||||
console.log(codes(word, 4, 0.5, 3), "|", codes(word, 3, 9.75, -2));
|
||||
const negativeZero = -0;
|
||||
console.log(codes(word, 2, negativeZero, 3), Object.is(negativeZero, -0));
|
||||
console.log(codes(word, 2, NaN, 3), "|", codes(word, 2, Infinity, -2), "|", codes(word, 2, -Infinity, 3));
|
||||
// Near 2^52 the run-time test passes or fails on the trip count; near 2^53
|
||||
// the double counter rounds (odd values above 2^53 are not representable).
|
||||
console.log(codes(word, 4, 2 ** 52 - 16, 3));
|
||||
console.log(codes(word, 6, 2 ** 52 - 4, 3));
|
||||
console.log(codes(word, 6, 2 ** 53 - 10, 3));
|
||||
console.log(codes(word, 4, -(2 ** 53) + 3, -2));
|
||||
// A limit that is not finite or not an integer.
|
||||
function capped(s: string, limit: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, j = 1; i < limit; i++, j += 2) {
|
||||
if (j >= s.length) break;
|
||||
out.push(s.charCodeAt(j));
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(capped(word, Infinity), "|", capped(word, 3.5), "|", capped(word, NaN), "|", capped(word, -Infinity));
|
||||
|
||||
// A descending driver with an ascending follower, inclusive bounds and
|
||||
// larger steps.
|
||||
function mirror(s: string): string {
|
||||
const out: string[] = [];
|
||||
for (let i = s.length - 1, j = 0; i >= 0; i--, j += 2) out.push(`${s.charCodeAt(i)}@${j}`);
|
||||
return out.join(" ");
|
||||
}
|
||||
console.log(mirror("scriptc"));
|
||||
function stepped(bytes: Uint8Array, last: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, j = bytes.length - 1; i <= last; i += 3, j -= 5)
|
||||
out.push(bytes[i]! | 0, bytes[j]! | 0, j);
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(stepped(px, 9), "|", stepped(px, 30));
|
||||
|
||||
// Three counters, continue and break.
|
||||
function triple(a: Uint8Array, n: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, j = 10, k = 100; i < n; i++, j -= 1, k += 7) {
|
||||
if (i % 3 === 1) continue;
|
||||
if (k > 150) break;
|
||||
out.push(a[i]!, a[j]!, k);
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(triple(px, 12), "|", triple(px, 4));
|
||||
|
||||
// A counted limit with a known range and a follower used by a remainder.
|
||||
let rotated = 0;
|
||||
for (let i = 0, j = 5; i < 1000; i++, j += 3) rotated = (rotated * 31 + px[j % 32]!) % 1000003;
|
||||
console.log(rotated);
|
||||
|
||||
// The follower starting from a driver's start of unproven exactness.
|
||||
function window(bytes: Uint8Array, from: number, to: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = from, j = 0; i < to; i++, j += 2) out.push(bytes[i]! | 0, bytes[j]! | 0, i, j);
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(window(px, 2, 5), "|", window(px, 1.5, 4), "|", window(px, -0, 2));
|
||||
|
||||
// Counters a closure captures keep one binding per iteration; a follower the
|
||||
// body writes, one the condition reads, and a start that reads another
|
||||
// counter keep their JS values.
|
||||
const reads: (() => number)[] = [];
|
||||
for (let i = 0, j = 0; i < 3; i++, j += 4) {
|
||||
reads.push(() => j);
|
||||
void px[j];
|
||||
}
|
||||
console.log(reads.map((read) => read()).join(","));
|
||||
const written: number[] = [];
|
||||
for (let i = 0, j = 0; i < 5; i++, j += 2) {
|
||||
if (i === 2) j += 7;
|
||||
written.push(px[j]!, j);
|
||||
}
|
||||
console.log(written.join(","));
|
||||
const chased: number[] = [];
|
||||
for (let i = 0, j = 5; i < j; i += 2, j++) chased.push(px[i]!, i, j);
|
||||
console.log(chased.join(","));
|
||||
const offset: number[] = [];
|
||||
for (let i = 1, j = i + 1; i < 4; i++, j += 2) offset.push(px[j]!, j);
|
||||
console.log(offset.join(","));
|
||||
|
||||
// Counters that live across a yield or an await.
|
||||
function* strided(bytes: Uint8Array, n: number): Generator<number> {
|
||||
for (let i = 0, j = 1; i < n; i++, j += 3) yield bytes[j]! + i;
|
||||
}
|
||||
console.log([...strided(px, 6)].join(","));
|
||||
async function weighted(bytes: Uint8Array, n: number): Promise<number> {
|
||||
let sum = 0;
|
||||
for (let i = 0, j = 2; i < n; i++, j += 5) {
|
||||
await null;
|
||||
sum += bytes[j]! * (i + 1);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
void (async () => console.log(await weighted(px, 6), await weighted(px, 3)))();
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4260, "san": 6600 }
|
||||
Reference in New Issue
Block a user