Run two-counter while loops on integer counters (#1946)

This commit is contained in:
Chris Tate
2026-10-22 23:28:18 -05:00
committed by GitHub
8 changed files with 765 additions and 9 deletions
@@ -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 },
+11 -8
View File
@@ -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;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7144-paired-while-counters.ts"
],
"diags": []
}
+244
View File
@@ -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 }