mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Run two-counter while loops on integer counters (#1946)
This commit is contained in:
@@ -92,6 +92,7 @@ import {
|
||||
type IntegerRanges,
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
@@ -527,7 +528,9 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -402,6 +402,16 @@ test("paired counters closing in on each other run as integers", () => {
|
||||
guarded: false,
|
||||
range: { min: 0, max: 9 },
|
||||
});
|
||||
// `hi > lo` and `hi >= lo` are `lo < hi` and `lo <= hi`.
|
||||
for (const op of [">", ">="] as const) {
|
||||
const reversed = paired(number(0), number(9));
|
||||
reversed.cond = { kind: "bin", op, left: ref("hi"), right: ref("lo"), type: BOOL, loc };
|
||||
expect(matchIntegerPairedForLoop(reversed, vars)).toMatchObject({
|
||||
low: { localId: "lo", step: 1 },
|
||||
high: { localId: "hi", step: -1 },
|
||||
inclusive: op === ">=",
|
||||
});
|
||||
}
|
||||
// A start without a known range is tested at run time.
|
||||
expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({
|
||||
high: { startGuarded: true },
|
||||
|
||||
@@ -382,9 +382,9 @@ export function matchIntegerCountedForLoop(
|
||||
*
|
||||
* The first counter only grows and the second only shrinks, by positive
|
||||
* literal steps, the body writes neither, and the loop runs while the first
|
||||
* is below (or at most) the second. Every body entry therefore holds both
|
||||
* between the first start and the second start: exact integers when the
|
||||
* starts are. A start without a proven integer range is tested at run time
|
||||
* is below (or at most) the second, written either way round (`hi > lo`).
|
||||
* Every body entry therefore holds both between the first start and the
|
||||
* second start: exact integers when the starts are. A start without a proven integer range is tested at run time
|
||||
* on a versioned loop, as a counted loop's is. */
|
||||
export interface IntegerPairedCounter {
|
||||
localId: string;
|
||||
@@ -412,10 +412,13 @@ export function matchIntegerPairedForLoop(
|
||||
cond = stmt.cond;
|
||||
if (init?.kind !== "block" || init.body.length !== 2) return null;
|
||||
if (update?.kind !== "block" || update.body.length !== 2) return null;
|
||||
if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null;
|
||||
if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null;
|
||||
const lowId = cond.left.localId,
|
||||
highId = cond.right.localId;
|
||||
if (cond?.kind !== "bin" || cond.left.kind !== "varRef" || cond.right.kind !== "varRef")
|
||||
return null;
|
||||
const below = cond.op === "<" || cond.op === "<=";
|
||||
if (!below && cond.op !== ">" && cond.op !== ">=") return null;
|
||||
// `hi > lo` is `lo < hi`.
|
||||
const lowId = below ? cond.left.localId : cond.right.localId,
|
||||
highId = below ? cond.right.localId : cond.left.localId;
|
||||
if (lowId === highId) return null;
|
||||
const counter = (id: string): IntegerPairedCounter | null => {
|
||||
const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id);
|
||||
@@ -485,7 +488,7 @@ export function matchIntegerPairedForLoop(
|
||||
return {
|
||||
low,
|
||||
high,
|
||||
inclusive: cond.op === "<=",
|
||||
inclusive: cond.op === "<=" || cond.op === ">=",
|
||||
guarded,
|
||||
// A loop whose starts already fail the condition never enters the body.
|
||||
range: {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
arrayOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { runPairedWhileLoopsAsFor } from "./paired-while-loops.js";
|
||||
|
||||
const loc = { file: "paired-while-loops.ts", start: 0, end: 0 };
|
||||
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const array = { kind: "varRef", localId: "a", type: arrayOf(F64), loc } as const;
|
||||
const element = (index: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayGet", arr: array, index, type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const step = (id: string, op: "+" | "-"): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: id,
|
||||
value: { kind: "bin", op, left: ref(id), right: number(1), type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const declare = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
|
||||
const number_ = (id: string): IrLocal => ({
|
||||
id,
|
||||
name: id.split(".")[0]!,
|
||||
type: F64,
|
||||
mutable: true,
|
||||
});
|
||||
|
||||
function whileLoop(
|
||||
body: IrStmt[] = [element(ref("lo.0")), element(ref("hi.0"))],
|
||||
updates: IrStmt[] = [step("lo.0", "+"), step("hi.0", "-")],
|
||||
): IrStmt & { kind: "while" } {
|
||||
return {
|
||||
kind: "while",
|
||||
cond: { kind: "bin", op: "<", left: ref("lo.0"), right: ref("hi.0"), type: BOOL, loc },
|
||||
body: [...body, ...updates],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function program(body: IrStmt[], extra: Partial<IrFunction> = {}): IrModule {
|
||||
const fn: IrFunction = {
|
||||
name: "f",
|
||||
params: [{ localId: "n.0", name: "n", type: F64 }],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
number_("n.0"),
|
||||
number_("lo.0"),
|
||||
number_("hi.0"),
|
||||
{ id: "a", name: "a", type: arrayOf(F64), mutable: false },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
...extra,
|
||||
};
|
||||
return { functions: [fn], globals: [] } as unknown as IrModule;
|
||||
}
|
||||
|
||||
const rewritten = (mod: IrModule): IrStmt[] => runPairedWhileLoopsAsFor(mod).functions[0]!.body;
|
||||
|
||||
test("a two-counter while loop runs as a for loop over new counters", () => {
|
||||
const mod = program([declare("lo.0", number(0)), declare("hi.0", ref("n.0")), whileLoop()]);
|
||||
const out = runPairedWhileLoopsAsFor(mod).functions[0]!;
|
||||
// The declarations stay; the loop's counters are new locals that start
|
||||
// from them, and the trailing updates are the for loop's update.
|
||||
expect(out.body.slice(0, 2)).toEqual(mod.functions[0]!.body.slice(0, 2));
|
||||
expect(out.body[2]).toMatchObject({
|
||||
kind: "for",
|
||||
init: {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: "lo.1", init: { kind: "varRef", localId: "lo.0" } },
|
||||
{ kind: "varDecl", localId: "hi.1", init: { kind: "varRef", localId: "hi.0" } },
|
||||
],
|
||||
},
|
||||
cond: { op: "<", left: { localId: "lo.1" }, right: { localId: "hi.1" } },
|
||||
update: { kind: "block", body: [{ localId: "lo.1" }, { localId: "hi.1" }] },
|
||||
body: [
|
||||
{ expr: { kind: "arrayGet", index: { localId: "lo.1" } } },
|
||||
{ expr: { kind: "arrayGet", index: { localId: "hi.1" } } },
|
||||
],
|
||||
});
|
||||
expect(out.locals.map((l) => l.id)).toEqual(["n.0", "lo.0", "hi.0", "a", "lo.1", "hi.1"]);
|
||||
});
|
||||
|
||||
test("parameters, reversed comparisons, labels and either update order qualify", () => {
|
||||
const loop = whileLoop(undefined, [step("hi.0", "-"), step("lo.0", "+")]);
|
||||
loop.cond = { kind: "bin", op: ">=", left: ref("hi.0"), right: ref("lo.0"), type: BOOL, loc };
|
||||
loop.labels = ["outer"];
|
||||
const mod = program([declare("lo.0", number(0)), loop], {
|
||||
params: [{ localId: "hi.0", name: "hi", type: F64 }],
|
||||
});
|
||||
expect(rewritten(mod)[1]).toMatchObject({
|
||||
kind: "for",
|
||||
labels: ["outer"],
|
||||
cond: { op: ">=", left: { localId: "hi.1" }, right: { localId: "lo.1" } },
|
||||
update: { body: [{ localId: "hi.1" }, { localId: "lo.1" }] },
|
||||
});
|
||||
});
|
||||
|
||||
test("loops the for form would change, or whose counters are used elsewhere, stay", () => {
|
||||
const decls = [declare("lo.0", number(0)), declare("hi.0", ref("n.0"))];
|
||||
const kept = (body: IrStmt[], extra: Partial<IrFunction> = {}): void => {
|
||||
const mod = program(body, extra);
|
||||
expect(runPairedWhileLoopsAsFor(mod)).toBe(mod);
|
||||
};
|
||||
// A continue that binds to the loop skips the trailing updates.
|
||||
kept([...decls, whileLoop([{ kind: "continue", loc }, element(ref("lo.0"))])]);
|
||||
const labeled = whileLoop([{ kind: "continue", label: "outer", loc }, element(ref("lo.0"))]);
|
||||
labeled.labels = ["outer"];
|
||||
kept([...decls, labeled]);
|
||||
// A counter read after the loop, written before it, or declared twice.
|
||||
kept([...decls, whileLoop(), { kind: "return", value: ref("lo.0"), loc }]);
|
||||
kept([...decls, step("hi.0", "-"), whileLoop()]);
|
||||
kept([...decls, declare("lo.0", number(1)), whileLoop()]);
|
||||
// A declaration outside an enclosing loop: a later entry would resume
|
||||
// from the values the previous run left.
|
||||
kept([
|
||||
...decls,
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [whileLoop()],
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
// A parameter counter inside another loop, for the same reason.
|
||||
const outerLoop: IrStmt = {
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [whileLoop()],
|
||||
loc,
|
||||
};
|
||||
kept([outerLoop], {
|
||||
params: [
|
||||
{ localId: "lo.0", name: "lo", type: F64 },
|
||||
{ localId: "hi.0", name: "hi", type: F64 },
|
||||
],
|
||||
});
|
||||
// A counter that was never declared, or that this function captures.
|
||||
kept([declare("lo.0", number(0)), whileLoop()]);
|
||||
kept([...decls, whileLoop()], { captures: [{ localId: "hi.0", name: "hi", type: F64 }] });
|
||||
// The body writes a counter, a counter is boxed, or no counter is an index.
|
||||
kept([...decls, whileLoop([element(ref("lo.0")), step("hi.0", "-")])]);
|
||||
const boxed = program([...decls, whileLoop()]);
|
||||
boxed.functions[0]!.locals[2] = { ...number_("hi.0"), boxed: true };
|
||||
expect(runPairedWhileLoopsAsFor(boxed)).toBe(boxed);
|
||||
kept([...decls, whileLoop([])]);
|
||||
});
|
||||
|
||||
test("a continue in a nested loop keeps the outer loop eligible", () => {
|
||||
const inner: IrStmt = {
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
body: [{ kind: "continue", loc }],
|
||||
loc,
|
||||
};
|
||||
// Literal starts have known ranges, so the loop needs no start test and
|
||||
// may hold a nested loop.
|
||||
const mod = program([
|
||||
declare("lo.0", number(0)),
|
||||
declare("hi.0", number(9)),
|
||||
whileLoop([inner, element(ref("lo.0"))]),
|
||||
]);
|
||||
expect(rewritten(mod)[2]).toMatchObject({ kind: "for", body: [{ kind: "while" }, {}] });
|
||||
// A start without a known range is tested at run time, on a loop that
|
||||
// may not nest another.
|
||||
const guarded = program([
|
||||
declare("lo.0", number(0)),
|
||||
declare("hi.0", ref("n.0")),
|
||||
whileLoop([inner, element(ref("lo.0"))]),
|
||||
]);
|
||||
expect(runPairedWhileLoopsAsFor(guarded)).toBe(guarded);
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import { matchIntegerPairedForLoop } from "./integer-loops.js";
|
||||
import { analyzeIntegerRanges, type IntegerRange } from "./integer-ranges.js";
|
||||
import {
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
type WhileLoop = IrStmt & { kind: "while" };
|
||||
|
||||
/** Two-counter while loops run as two-counter for loops.
|
||||
*
|
||||
* let lo = a, hi = b;
|
||||
* while (lo < hi) { ...; lo++; hi--; }
|
||||
*
|
||||
* runs as `for (let l = lo, h = hi; l < h; l++, h--) { ... }`, whose
|
||||
* counters the backend stores as integers (matchIntegerPairedForLoop). The
|
||||
* loop qualifies when:
|
||||
* - its condition compares the two counters (`<`, `<=`, `>` or `>=`);
|
||||
* - its body ends with one update of each counter;
|
||||
* - no `continue` in the body binds to it (a for loop's `continue` runs the
|
||||
* update, a while loop's skips the body's last statements);
|
||||
* - the function mentions neither counter outside the loop, except in one
|
||||
* declaration or as a parameter;
|
||||
* - every entry starts from the declared or passed values: a declaration is
|
||||
* an earlier statement of the loop's own statement list, which runs before
|
||||
* each entry, and a loop over a parameter is in no other loop.
|
||||
*
|
||||
* The for loop's counters are new locals that start from the counters'
|
||||
* values on entry, so the declarations stay where they are and nothing
|
||||
* between them and the loop moves. The original counters are not read
|
||||
* after the loop, so they need no final value. A loop that the paired match
|
||||
* would refuse is left as it is. */
|
||||
export function runPairedWhileLoopsAsFor(mod: IrModule): IrModule {
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const next = rewriteFunction(fn);
|
||||
if (next !== fn) changed = true;
|
||||
return next;
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
|
||||
/** The local a statement steps: `x++`, `x--` or `x = ...`. */
|
||||
function updatedLocal(s: IrStmt): string | null {
|
||||
if (s.kind === "assign") return s.localId;
|
||||
if (s.kind === "exprStmt" && s.expr.kind === "incDec") return s.expr.localId;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The counters of a loop with the qualifying condition and trailing
|
||||
* updates, or null. */
|
||||
function loopCounters(loop: WhileLoop, locals: ReadonlyMap<string, IrLocal>): string[] | null {
|
||||
const cond = loop.cond;
|
||||
if (cond.kind !== "bin") return null;
|
||||
if (cond.op !== "<" && cond.op !== "<=" && cond.op !== ">" && cond.op !== ">=") return null;
|
||||
if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null;
|
||||
const counters = [cond.left.localId, cond.right.localId];
|
||||
if (counters[0] === counters[1]) return null;
|
||||
const n = loop.body.length;
|
||||
if (n < 2) return null;
|
||||
const updated = [updatedLocal(loop.body[n - 2]!), updatedLocal(loop.body[n - 1]!)];
|
||||
if (!counters.every((id) => updated.includes(id))) return null;
|
||||
for (const id of counters) {
|
||||
const local = locals.get(id);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
}
|
||||
return counters;
|
||||
}
|
||||
|
||||
function isLoop(s: IrStmt): boolean {
|
||||
return s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
|
||||
}
|
||||
|
||||
/** True when a `continue` in the body binds to the loop: an unlabeled one
|
||||
* outside nested loops, or one that names a label of the loop. */
|
||||
function continuesLoop(loop: WhileLoop): boolean {
|
||||
const labels = loop.labels ?? [];
|
||||
let depth = 0;
|
||||
let found = false;
|
||||
const expr = (e: IrExpr): boolean => !found && everyExprChild(e, expr, stmt);
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (found) return false;
|
||||
if (s.kind === "continue") {
|
||||
if (s.label === undefined ? depth === 0 : labels.includes(s.label)) found = true;
|
||||
return !found;
|
||||
}
|
||||
const nested = isLoop(s);
|
||||
if (nested) depth++;
|
||||
const ok = everyStmtChild(s, expr, stmt);
|
||||
if (nested) depth--;
|
||||
return ok;
|
||||
};
|
||||
loop.body.every(stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Mentions of each id in `node`, in any field (a string literal equal to
|
||||
* an id counts too, which only refuses more loops). */
|
||||
function countMentions(
|
||||
node: unknown,
|
||||
ids: ReadonlySet<string>,
|
||||
mentions: Map<string, number>,
|
||||
): void {
|
||||
if (typeof node === "string") {
|
||||
if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1);
|
||||
} else if (Array.isArray(node)) {
|
||||
for (const item of node) countMentions(item, ids, mentions);
|
||||
} else if (node !== null && typeof node === "object") {
|
||||
const record = node as Record<string, unknown>;
|
||||
for (const key in record)
|
||||
if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions);
|
||||
}
|
||||
}
|
||||
|
||||
/** A loop with the qualifying shape, the statements before it in its
|
||||
* statement list, and whether it is inside another loop. */
|
||||
interface Candidate {
|
||||
loop: WhileLoop;
|
||||
counters: string[];
|
||||
before: readonly IrStmt[];
|
||||
nested: boolean;
|
||||
}
|
||||
|
||||
function findCandidates(fn: IrFunction, locals: ReadonlyMap<string, IrLocal>): Candidate[] {
|
||||
const candidates: Candidate[] = [];
|
||||
let depth = 0;
|
||||
const list = (body: readonly IrStmt[]): void => {
|
||||
body.forEach((s, i) => {
|
||||
if (s.kind === "while") {
|
||||
const counters = loopCounters(s, locals);
|
||||
if (counters !== null && !continuesLoop(s))
|
||||
candidates.push({ loop: s, counters, before: body.slice(0, i), nested: depth > 0 });
|
||||
}
|
||||
visit(s);
|
||||
});
|
||||
};
|
||||
// Statement lists inside expressions are not searched: their loops stay.
|
||||
const visit = (s: IrStmt): void => {
|
||||
const loop = isLoop(s);
|
||||
if (loop) depth++;
|
||||
switch (s.kind) {
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
list(s.body);
|
||||
break;
|
||||
case "if":
|
||||
list(s.then);
|
||||
if (s.else_ !== null) list(s.else_);
|
||||
break;
|
||||
case "switch":
|
||||
for (const c of s.cases) list(c.body);
|
||||
break;
|
||||
case "tryCatch":
|
||||
list(s.tryBody);
|
||||
if (s.catchBody !== null) list(s.catchBody);
|
||||
if (s.finallyBody !== null) list(s.finallyBody);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (loop) depth--;
|
||||
};
|
||||
list(fn.body);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const candidates = findCandidates(fn, locals);
|
||||
if (candidates.length === 0) return fn;
|
||||
|
||||
const ids = new Set<string>();
|
||||
for (const c of candidates) for (const id of c.counters) ids.add(id);
|
||||
const total = new Map<string, number>();
|
||||
countMentions(fn.body, ids, total);
|
||||
const params = new Set(fn.params.map((p) => p.localId));
|
||||
// A counter this function reaches through its environment.
|
||||
const outer = new Map<string, number>();
|
||||
countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer);
|
||||
|
||||
let ranges: ReadonlyMap<IrExpr, IntegerRange | null> | null = null;
|
||||
const used = new Set(fn.locals.map((l) => l.id));
|
||||
const added: IrLocal[] = [];
|
||||
const rewritten = new Map<WhileLoop, Map<string, string>>();
|
||||
for (const { loop, counters, before, nested } of candidates) {
|
||||
const inside = new Map<string, number>();
|
||||
countMentions(loop, new Set(counters), inside);
|
||||
// Each counter's start: its declaration's initializer, or null for a
|
||||
// parameter.
|
||||
const starts = new Map<string, IrExpr | null>();
|
||||
for (const id of counters) {
|
||||
if ((outer.get(id) ?? 0) !== 0) break;
|
||||
const elsewhere = (total.get(id) ?? 0) - (inside.get(id) ?? 0);
|
||||
if (params.has(id)) {
|
||||
if (elsewhere === 0 && !nested) starts.set(id, null);
|
||||
continue;
|
||||
}
|
||||
const decl = before.find((s) => s.kind === "varDecl" && s.localId === id);
|
||||
if (elsewhere === 1 && decl?.kind === "varDecl" && decl.init !== null)
|
||||
starts.set(id, decl.init);
|
||||
}
|
||||
if (starts.size !== counters.length) continue;
|
||||
const renamed = new Map<string, string>();
|
||||
const fresh: IrLocal[] = [];
|
||||
for (const id of counters) {
|
||||
const local = locals.get(id)!;
|
||||
let k = 0;
|
||||
while (used.has(`${local.name}.${k}`)) k++;
|
||||
const next = `${local.name}.${k}`;
|
||||
used.add(next);
|
||||
renamed.set(id, next);
|
||||
fresh.push({ ...local, id: next });
|
||||
}
|
||||
const candidate = pairedForLoop(loop, renamed);
|
||||
const forLocals = new Map(locals);
|
||||
for (const local of fresh) forLocals.set(local.id, local);
|
||||
// A start reads its counter on entry, which holds the initializer's
|
||||
// value, with that value's range.
|
||||
ranges ??= analyzeIntegerRanges(fn);
|
||||
const startRanges = new Map<IrExpr, IntegerRange | null>();
|
||||
if (candidate.init?.kind === "block")
|
||||
for (const decl of candidate.init.body) {
|
||||
if (decl.kind !== "varDecl" || decl.init?.kind !== "varRef") continue;
|
||||
const init = starts.get(decl.init.localId);
|
||||
if (!init) continue;
|
||||
const literal =
|
||||
init.kind === "numLit" && Number.isSafeInteger(init.value) && !Object.is(init.value, -0);
|
||||
startRanges.set(
|
||||
decl.init,
|
||||
literal ? { min: init.value, max: init.value } : (ranges.get(init) ?? null),
|
||||
);
|
||||
}
|
||||
if (matchIntegerPairedForLoop(candidate, forLocals, startRanges) === null) {
|
||||
for (const local of fresh) used.delete(local.id);
|
||||
continue;
|
||||
}
|
||||
rewritten.set(loop, renamed);
|
||||
added.push(...fresh);
|
||||
}
|
||||
if (rewritten.size === 0) return fn;
|
||||
|
||||
const expr = (e: IrExpr): IrExpr => mapExprChildren(e, expr, stmt);
|
||||
const stmt = (s: IrStmt): IrStmt => {
|
||||
const next = mapStmtChildren(s, expr, stmt);
|
||||
const renamed = s.kind === "while" ? rewritten.get(s) : undefined;
|
||||
// The body may hold rewritten loops of its own.
|
||||
return renamed === undefined ? next : pairedForLoop(next as WhileLoop, renamed);
|
||||
};
|
||||
return { ...fn, body: fn.body.map(stmt), locals: [...fn.locals, ...added] };
|
||||
}
|
||||
|
||||
/** The for loop over the renamed counters: their starts read the original
|
||||
* counters, and the body's two trailing updates become the update. */
|
||||
function pairedForLoop(
|
||||
loop: WhileLoop,
|
||||
renamed: ReadonlyMap<string, string>,
|
||||
): IrStmt & { kind: "for" } {
|
||||
const expr = (e: IrExpr): IrExpr => {
|
||||
const next = mapExprChildren(e, expr, stmt);
|
||||
if (next.kind === "varRef" || next.kind === "incDec" || next.kind === "assignExpr") {
|
||||
const id = renamed.get(next.localId);
|
||||
if (id !== undefined) return { ...next, localId: id };
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const stmt = (s: IrStmt): IrStmt => {
|
||||
const next = mapStmtChildren(s, expr, stmt);
|
||||
if (
|
||||
next.kind === "varDecl" ||
|
||||
next.kind === "assign" ||
|
||||
next.kind === "forOf" ||
|
||||
next.kind === "rethrow"
|
||||
) {
|
||||
const id = renamed.get(next.localId);
|
||||
if (id !== undefined) return { ...next, localId: id };
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const loc = loop.loc;
|
||||
const n = loop.body.length;
|
||||
const declarations: IrStmt[] = [];
|
||||
for (const [id, next] of renamed)
|
||||
declarations.push({
|
||||
kind: "varDecl",
|
||||
localId: next,
|
||||
init: { kind: "varRef", localId: id, type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const result: IrStmt & { kind: "for" } = {
|
||||
kind: "for",
|
||||
init: { kind: "block", body: declarations, loc },
|
||||
cond: expr(loop.cond),
|
||||
update: { kind: "block", body: loop.body.slice(n - 2).map(stmt), loc },
|
||||
body: loop.body.slice(0, n - 2).map(stmt),
|
||||
loc,
|
||||
};
|
||||
if (loop.labels !== undefined) result.labels = loop.labels;
|
||||
return result;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7144-paired-while-counters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// While loops with two counters closing in on each other (`lo++; hi--;` at
|
||||
// the end of the body): reversals over arrays, typed arrays and strings,
|
||||
// either comparison direction, inclusive bounds, larger steps, parameters as
|
||||
// counters, early exits, unproven starts, and the shapes that keep the loop
|
||||
// as it is (a continue, a counter read after the loop, a captured counter,
|
||||
// a counter the body writes).
|
||||
function reverse(a: number[], from: number, to: number): number[] {
|
||||
let lo = from;
|
||||
let hi = to;
|
||||
while (lo < hi) {
|
||||
const t = a[lo]!;
|
||||
a[lo] = a[hi]!;
|
||||
a[hi] = t;
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
console.log(reverse(base(), 0, 9).join(","));
|
||||
console.log(reverse(base(), 2, 6).join(","));
|
||||
console.log(reverse(base(), 6, 2).join(","));
|
||||
console.log(reverse(base(), 3, 3).join(","));
|
||||
console.log(reverse(base(), -0, 2).join(","), reverse(base(), 7, -0).join(","));
|
||||
|
||||
// The parameters are the counters; the updates come in the other order.
|
||||
function reverseRange(a: Float64Array, lo: number, hi: number): string {
|
||||
while (hi > lo) {
|
||||
const t = a[lo]!;
|
||||
a[lo] = a[hi]!;
|
||||
a[hi] = t;
|
||||
hi -= 1;
|
||||
++lo;
|
||||
}
|
||||
return a.join(",");
|
||||
}
|
||||
console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 0, 5));
|
||||
console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 1, 3.5));
|
||||
console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), -2, 2));
|
||||
console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), NaN, 4), reverseRange(new Float64Array([1, 2]), 0, NaN));
|
||||
|
||||
function bytesReversed(text: string): string {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let i = 0,
|
||||
j = bytes.length - 1;
|
||||
while (i < j) {
|
||||
const t = bytes[i]!;
|
||||
bytes[i] = bytes[j]!;
|
||||
bytes[j] = t;
|
||||
i++;
|
||||
j--;
|
||||
}
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
console.log(bytesReversed("hello, world"), bytesReversed(""), bytesReversed("x"));
|
||||
|
||||
// Unproven starts read through charCodeAt (which truncates its index).
|
||||
function codes(s: string, from: number, to: number): string {
|
||||
const out: number[] = [];
|
||||
let lo = from;
|
||||
let hi = to;
|
||||
while (lo <= hi) {
|
||||
out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi);
|
||||
lo += 2;
|
||||
hi -= 3;
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
const word = "abcdefghijkl";
|
||||
console.log(codes(word, 0, 11), "|", codes(word, 0.5, 9.25), "|", codes(word, 4, 4));
|
||||
console.log(codes(word, 2 ** 53 - 3, 2 ** 53), "|", codes(word, -3, 2), "|", codes(word, 5, 1));
|
||||
|
||||
// Negative zero starts stay negative zero.
|
||||
function signs(text: string, from: number): string {
|
||||
const out: string[] = [];
|
||||
let lo = from;
|
||||
let hi = text.length - 1;
|
||||
while (lo < hi) {
|
||||
out.push(`${Object.is(lo, -0)}:${text.charCodeAt(lo)}`);
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
let a = -0;
|
||||
let b = 2;
|
||||
while (a < b) {
|
||||
out.push(`${Object.is(a, -0)}:${text.charCodeAt(b)}`);
|
||||
a++;
|
||||
b--;
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
console.log(signs("abcd", -0), signs("abcd", 0));
|
||||
|
||||
function isPalindrome(s: string): boolean {
|
||||
let i = 0;
|
||||
let j = s.length - 1;
|
||||
while (i < j) {
|
||||
if (s.charCodeAt(i) !== s.charCodeAt(j)) return false;
|
||||
i++;
|
||||
j--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
console.log(isPalindrome("racecar"), isPalindrome("abca"), isPalindrome(""), isPalindrome("ab"));
|
||||
|
||||
function firstMismatch(a: number[]): number {
|
||||
let found = -1;
|
||||
let lo = 0;
|
||||
let hi = a.length - 1;
|
||||
while (lo < hi) {
|
||||
const left = a[lo]!;
|
||||
if (left !== a[hi]!) {
|
||||
found = lo;
|
||||
break;
|
||||
}
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
console.log(firstMismatch([1, 2, 3, 2, 1]), firstMismatch([1, 2, 3, 4, 2, 1]), firstMismatch([]));
|
||||
|
||||
// A labeled jump to an outer loop leaves the inner loop from its middle.
|
||||
function rows(grid: number[][]): string {
|
||||
const out: string[] = [];
|
||||
outer: for (const row of grid) {
|
||||
let lo = 0;
|
||||
let hi = row.length - 1;
|
||||
while (lo < hi) {
|
||||
const left = row[lo]!;
|
||||
const right = row[hi]!;
|
||||
if (left === 0) continue outer;
|
||||
if (right === 0) break outer;
|
||||
out.push(`${left}${right}`);
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
out.push("|");
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
console.log(rows([[1, 2, 3, 4], [0, 5, 6], [7, 8, 9, 1], [2, 3, 0, 4]]));
|
||||
|
||||
// Repeated entries: the counters are declared inside an outer loop. Each
|
||||
// pass reverses the prefix that ends at the index the first element holds,
|
||||
// until the first element is 0, and the passes are counted.
|
||||
function prefixReversals(values: number[]): number {
|
||||
const copy = values.slice();
|
||||
let count = 0;
|
||||
let end = copy[0]!;
|
||||
while (end !== 0) {
|
||||
let i = 0;
|
||||
let j = end;
|
||||
while (i < j) {
|
||||
const t = copy[i]!;
|
||||
copy[i] = copy[j]!;
|
||||
copy[j] = t;
|
||||
i++;
|
||||
j--;
|
||||
}
|
||||
count++;
|
||||
end = copy[0]!;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
console.log(
|
||||
prefixReversals([3, 1, 2, 0]),
|
||||
prefixReversals([0, 1, 2]),
|
||||
prefixReversals([4, 3, 2, 1, 0]),
|
||||
prefixReversals([2, 0, 1]),
|
||||
);
|
||||
|
||||
// Holes and present undefined values move as they are.
|
||||
const sparse: (number | undefined)[] = [1, , 3, undefined, 5];
|
||||
{
|
||||
let lo = 0;
|
||||
let hi = sparse.length - 1;
|
||||
while (lo < hi) {
|
||||
const t = sparse[lo];
|
||||
sparse[lo] = sparse[hi];
|
||||
sparse[hi] = t;
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
}
|
||||
console.log(sparse, 1 in sparse, 3 in sparse);
|
||||
|
||||
// These keep the while loop: a continue skips the trailing updates, a
|
||||
// counter is read after the loop, a closure captures a counter, and the
|
||||
// body writes a counter.
|
||||
function skipping(n: number): string {
|
||||
const out: number[] = [];
|
||||
let lo = 0;
|
||||
let hi = n;
|
||||
while (lo < hi) {
|
||||
if (lo % 3 === 0) {
|
||||
lo += 2;
|
||||
continue;
|
||||
}
|
||||
out.push(lo, hi);
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(skipping(12));
|
||||
|
||||
function meet(n: number): string {
|
||||
let lo = 0;
|
||||
let hi = n;
|
||||
while (lo < hi) {
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
return `${lo}:${hi}`;
|
||||
}
|
||||
console.log(meet(9), meet(10), meet(0.5));
|
||||
|
||||
function captured(n: number): string {
|
||||
const seen: (() => number)[] = [];
|
||||
let lo = 0;
|
||||
let hi = n;
|
||||
while (lo < hi) {
|
||||
seen.push(() => lo * 100 + hi);
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
return seen.map((f) => f()).join(",");
|
||||
}
|
||||
console.log(captured(5));
|
||||
|
||||
function jumps(a: number[]): string {
|
||||
const out: number[] = [];
|
||||
let lo = 0;
|
||||
let hi = a.length - 1;
|
||||
while (lo < hi) {
|
||||
out.push(a[lo]!, a[hi]!);
|
||||
if (a[lo]! > 5) hi = lo;
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(jumps([1, 2, 9, 4, 5, 6, 7, 8]));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 4600 }
|
||||
Reference in New Issue
Block a user