diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 02943b852..ca2dd6b92 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -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 { diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts index 32c9cccab..f7a4bc892 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -120,6 +120,7 @@ const counted = ( limitGuarded: false, startGuarded: false, guardLimit: 0, + followers: [], ...fields, }); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 4fe1a27bd..dc91da2fb 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 24bbf5be0..b6ac27e3a 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -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 }, + ]); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index e80680d6e..c451c0852 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -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 | 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)[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, }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json new file mode 100644 index 000000000..392c9f10d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7141-comoving-loop-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7141-comoving-loop-counters.ts b/tests/corpus/7141-comoving-loop-counters.ts new file mode 100644 index 000000000..a756b7a46 --- /dev/null +++ b/tests/corpus/7141-comoving-loop-counters.ts @@ -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 { + 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 { + 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)))(); diff --git a/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json b/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json new file mode 100644 index 000000000..7c0606885 --- /dev/null +++ b/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 4260, "san": 6600 }