Run counters that step beside a counted loop's counter as integers (#1829)

This commit is contained in:
Chris Tate
2026-10-22 10:00:28 -05:00
committed by GitHub
8 changed files with 485 additions and 54 deletions
@@ -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 },
]);
});
+140 -50
View File
@@ -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,
};
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7141-comoving-loop-counters.ts"
],
"diags": []
}
+144
View File
@@ -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 }