diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7732fb8be0..08f2267bda 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2992,7 +2992,12 @@ function emitProvenLoop( } const deferred = new Set(); for (const id of plan.locals.keys()) deferred.add(id); - emitter.speculation = { writes: plan.writes, resume: new Map(), deferred }; + emitter.speculation = { + writes: plan.writes, + resume: new Map(), + deferred, + increments: plan.increments, + }; try { const leave = B.newLabel("integer.leave"); emitCopy(leave); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index e6c1b98c59..2b9b45ed04 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1775,6 +1775,9 @@ export class LlEmitter { ran?: string; /** Writes that speculate a rewritten value (rewriteSpeculable). */ values?: ReadonlyMap; + /** A proven integer loop's expression-position `++`/`--` of its + * shadowed bindings (ProvenLoopPlan.increments). */ + increments?: ReadonlySet; } | null = null; /** Speculative loop sites numbered so far in the module. */ speculativeLoopSites = 0; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 088d392d60..63db3d67b6 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -517,6 +517,29 @@ export function emitOperatorExpr( // Expression-position ++/-- over an f64 binding (locals, module // globals, capture boxes): read, write ±1, yield old (postfix) or // new (prefix). + const shadow = host.integerLoopBindings.get(e.localId); + if (shadow !== undefined && host.speculation?.increments?.has(e)) { + // A proven integer loop's shadow: the loop range holds every value + // the binding takes, so the step cannot overflow. + const old = B.tmp(); + const next = B.tmp(); + const number = B.tmp(); + B.line(`${old} = load i64, ptr ${shadow.slot}`); + B.line(`${next} = ${e.op === "+" ? "add" : "sub"} nsw i64 ${old}, 1`); + B.line(`store i64 ${next}, ptr ${shadow.slot}`); + const value = e.prefix ? next : old; + B.line(`${number} = sitofp i64 ${value} to double`); + return { + name: number, + type: e.type, + integer: { + name: value, + type: "i64", + signed: true, + range: shadow.range ?? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }, + }, + }; + } const b = host.binding(e.localId); if (host.int32Locals.has(e.localId)) { if (!host.int32LocalWrites.has(e)) diff --git a/packages/compiler/src/backend/llvm/loop-entry-versions.ts b/packages/compiler/src/backend/llvm/loop-entry-versions.ts index 2c448cae45..662146cb63 100644 --- a/packages/compiler/src/backend/llvm/loop-entry-versions.ts +++ b/packages/compiler/src/backend/llvm/loop-entry-versions.ts @@ -1,4 +1,4 @@ -/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while loop whose +/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while or for loop whose * integer work depends on locals entering it without range facts runs as * two copies. A test on entry checks that each such local holds an integer * in its assumed range; the fast copy is emitted under the facts that hold @@ -16,7 +16,7 @@ interface LoopEntryState { fn: IrFunction | null; facts: IntegerRangeFacts | null; slots: IntegerSlotFacts; - /** Found on the first while loop the function emits. */ + /** Found on the first while or for loop the function emits. */ versions: Map | null; /** Loops whose copies are being emitted: a loop is versioned once. */ readonly versioning: Set; @@ -61,7 +61,7 @@ export function beginLoopEntryVersions( * its locals: plain double slots of the current function, outside a * speculative copy and a coroutine. */ export function loopEntryVersion(emitter: LlEmitter, loop: IrStmt): LoopEntryVersion | null { - if (loop.kind !== "while") return null; + if (loop.kind !== "while" && loop.kind !== "for") return null; const state = ENTRY_VERSIONS.of(emitter); if (state.fn === null || state.facts === null || state.versioning.has(loop)) return null; if ( diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index 971bd00cbd..1a2b220b0c 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -773,6 +773,9 @@ export interface ProvenLoopPlan { locals: Map; /** Statement-level writes computed as integers. */ writes: Set; + /** Expression-position `++`/`--` of shadowed bindings (`out[o++] = v`), + * computed on the shadows. */ + increments: Set; } /** Plan a proven integer loop. `cursors` are the loop's bounded bindings, @@ -791,10 +794,17 @@ export function planProvenIntegerLoop( ): ProvenLoopPlan | null { if (cursors.size === 0) return null; const writes = new Map(); + const increments = new Map(); const refused = new Set(); let rejected = false; const expr = (e: IrExpr): boolean => { - if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId); + // Every value a cursor holds in the loop lies in its loop range, so an + // increment of one writes an integer of that range. + if (e.kind === "incDec" && cursors.has(e.localId)) { + const list = increments.get(e.localId) ?? []; + list.push(e); + increments.set(e.localId, list); + } else if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId); return everyExprChild(e, expr, nested); }; const nested = (s: IrStmt): boolean => { @@ -827,12 +837,13 @@ export function planProvenIntegerLoop( for (const s of loop.body) stmt(s); if (rejected) return null; const locals = new Map(); - for (const [id, list] of writes) { + for (const id of new Set([...writes.keys(), ...increments.keys()])) { if (refused.has(id) || !eligible(id)) continue; - const exact = list.every((s) => { - const value = speculativeWriteTarget(s)!.value!; - return isSpeculableValue(value, false) && proven(value); - }); + const exact = + (writes.get(id) ?? []).every((s) => { + const value = speculativeWriteTarget(s)!.value!; + return isSpeculableValue(value, false) && proven(value); + }) && (increments.get(id) ?? []).every(proven); if (exact) locals.set(id, cursors.get(id)!); } // Every binding a shadowed write reads is shadowed, already an integer, @@ -844,15 +855,19 @@ export function planProvenIntegerLoop( for (let changed = true; changed;) { changed = false; for (const id of [...locals.keys()]) - if (!writes.get(id)!.every((s) => grounded(speculativeWriteTarget(s)!.value!))) { + if (!(writes.get(id) ?? []).every((s) => grounded(speculativeWriteTarget(s)!.value!))) { locals.delete(id); changed = true; } } if (locals.size === 0) return null; const planned = new Set(); - for (const id of locals.keys()) for (const s of writes.get(id)!) planned.add(s); - return { locals, writes: planned }; + const plannedIncrements = new Set(); + for (const id of locals.keys()) { + for (const s of writes.get(id) ?? []) planned.add(s); + for (const e of increments.get(id) ?? []) plannedIncrements.add(e); + } + return { locals, writes: planned, increments: plannedIncrements }; } const SAFE = Number.MAX_SAFE_INTEGER; diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 23e2df90a9..f715323819 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -23,7 +23,8 @@ export interface IntegerRangeFacts { ranges: IntegerRanges; loopRanges: ReadonlyMap>; } -/** Ranges assumed for locals on entry to while loops (loop-entry-ranges.ts): +/** Ranges assumed for locals on entry to while and for loops + * (loop-entry-ranges.ts), after a for loop's initializer: * the facts inside such a loop hold in a copy of it that runs only after a * test proves every assumed local is an integer in its range. After the * loop, its assumed locals and its writes carry no facts. */ @@ -744,8 +745,11 @@ export function analyzeIntegerRangeFacts( set(facts, e.localId, range); break; case "incDec": { - const before = facts.get(e.localId) ?? null; + // A binding a for loop bounds holds a value in its loop range at + // every point of the loop, also where a statement around this + // increment has discarded its fact. const bound = counterRanges.get(e.localId); + const before = facts.get(e.localId) ?? bound ?? null; const sum = before ? integerArithmeticRange(e.op, before, { min: 1, max: 1 }) : null; const after = bound && sum @@ -1005,6 +1009,9 @@ export function analyzeIntegerRangeFacts( } case "for": { if (s.init) body([s.init], facts); + // The initializer writes none of the assumed locals. + const assumed = entries?.get(s); + if (assumed) for (const [id, range] of assumed) if (!facts.has(id)) set(facts, id, range); const counter = induction(s, facts); const bounded = boundedIntegerLoopFacts(s, loopLocals, facts); const loop = new Map(facts); @@ -1076,6 +1083,10 @@ export function analyzeIntegerRangeFacts( body([s.update], entry); } havoc(facts, [s], facts); + if (assumed) { + invalidate(facts, [s]); + for (const id of assumed.keys()) facts.delete(id); + } break; } case "while": diff --git a/packages/compiler/src/ir/loop-entry-ranges.test.ts b/packages/compiler/src/ir/loop-entry-ranges.test.ts index ad1b78ef62..1c15e1f422 100644 --- a/packages/compiler/src/ir/loop-entry-ranges.test.ts +++ b/packages/compiler/src/ir/loop-entry-ranges.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "vitest"; import { analyzeIntegerRangeFacts } from "./integer-ranges.js"; import { findLoopEntryVersions, LOOP_ENTRY_RANGE } from "./loop-entry-ranges.js"; -import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "./ir.js"; +import { BOOL, BYTES_U8, F64, type IrExpr, type IrFunction, type IrStmt } from "./ir.js"; const loc = { file: "test.ts", start: 0, end: 0 }; const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 }); @@ -141,3 +141,77 @@ test("loops without a remainder the assumption proves keep one copy", () => { const known = fn([set("acc", ref("unknown")), set("i", num(0)), proven], ["acc", "i"]); expect(findLoopEntryVersions(known, analyzeIntegerRangeFacts(known)).size).toBe(0); }); + +const step = (localId: string): IrExpr => ({ + loc, + kind: "incDec", + op: "+", + prefix: false, + localId, + type: F64, +}); +const bytes = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: BYTES_U8 }); + +/** i = unknown; o = unknown; line = unknown; + * for (let j = 0; j < line; j = j + 1) out[o++] = src[i++]; + * observe(o) */ +function cursors(init?: IrStmt) { + const read: IrExpr = { + loc, + kind: "bytesIntrinsic", + method: "get", + receiver: bytes("src"), + args: [step("i")], + type: F64, + }; + const store: IrStmt = { loc, kind: "bytesSet", arr: bytes("out"), index: step("o"), value: read }; + const loop: IrStmt = { + loc, + kind: "for", + init: init ?? { loc, kind: "varDecl", localId: "j", init: num(0) }, + cond: condition("<", ref("j"), ref("line")), + update: set("j", bin("+", ref("j"), num(1))), + body: [store], + }; + const after = ref("o"); + const f = fn( + [ + set("i", ref("unknown")), + set("o", ref("unknown")), + set("line", ref("unknown")), + loop, + observe(after), + ], + ["i", "o", "line", "j", "unknown"], + ); + f.locals.push( + { id: "src", name: "src", type: BYTES_U8, mutable: false }, + { id: "out", name: "out", type: BYTES_U8, mutable: false }, + ); + return { f, loop, store, read, after }; +} + +test("versions a for loop on the positions it advances and the passes it runs", () => { + const { f, loop, store, read, after } = cursors(); + const facts = analyzeIntegerRangeFacts(f); + expect(facts.ranges.get((store as Extract).index)).toBeNull(); + const version = findLoopEntryVersions(f, facts).get(loop)!; + expect([...version.entry.keys()].sort()).toEqual(["i", "line", "o"]); + // Every pass advances each position once, and the passes stay below `line`. + expect(version.loopRanges?.get("o")).toEqual({ + min: LOOP_ENTRY_RANGE.min, + max: 2 * LOOP_ENTRY_RANGE.max, + }); + expect(version.ranges.get((store as Extract).index)).toBeDefined(); + expect( + version.ranges.get((read as Extract).args[0]!), + ).toBeDefined(); + // No fact leaves the loop. + expect(version.ranges.has(after)).toBe(false); +}); + +test("leaves a for loop whose initializer writes an advanced position as it is", () => { + const init: IrStmt = { loc, kind: "assign", localId: "o", value: num(0) }; + const { f, loop } = cursors(init); + expect(findLoopEntryVersions(f, analyzeIntegerRangeFacts(f)).has(loop)).toBe(false); +}); diff --git a/packages/compiler/src/ir/loop-entry-ranges.ts b/packages/compiler/src/ir/loop-entry-ranges.ts index a5b7628be9..a9707c670a 100644 --- a/packages/compiler/src/ir/loop-entry-ranges.ts +++ b/packages/compiler/src/ir/loop-entry-ranges.ts @@ -1,5 +1,6 @@ import type { IrExpr, IrFunction, IrStmt } from "./ir.js"; import { everyExpr, everyStmt, everyStmtList } from "./traverse.js"; +import { byteNumberAccess } from "./byte-numbers.js"; import { analyzeIntegerRangeFacts, type IntegerRange, @@ -8,14 +9,12 @@ import { type LoopEntryAssumptions, } from "./integer-ranges.js"; -type WhileLoop = Extract; - /** The range a versioned loop requires of each assumed local on entry. A * loop's counters stay below a typed array or string length (at most 2^48), * so sums of a few such values and assumed locals stay exact integers. */ export const LOOP_ENTRY_RANGE: IntegerRange = { min: -(2 ** 48), max: 2 ** 48 }; -/** A copy of a while loop that runs when every local in `entry` holds an +/** A copy of a loop that runs when every local in `entry` holds an * integer in its range on entry (never -0). `ranges` are the facts that * hold for the loop's expressions in that copy and that the function's own * analysis does not prove; `loopRanges` are the loop's bounded bindings @@ -26,17 +25,22 @@ export interface LoopEntryVersion { loopRanges: ReadonlyMap | undefined; } -/** Versions of innermost while loops whose integer work depends on locals - * that enter the loop without range facts: copies of module globals, +/** Versions of innermost while and for loops whose integer work depends on + * locals that enter the loop without range facts: copies of module globals, * parameters, fields or call results (`const base = offset;` before a loop - * that computes `(count + base) % 60`). + * that computes `(count + base) % 60`), or positions carried from one loop + * to the next (`out[o++] = f(src[i++])` in a loop of `line` passes). * * The function is analyzed again with each number local the loop reads * without a fact assumed in LOOP_ENTRY_RANGE at the loop's entry. A loop is - * versioned when that analysis proves both operands of a remainder that the - * function's own analysis leaves unproven. Only remainders qualify: the - * operands of a product or a sum of such locals are as often fractional, - * and a loop whose test fails runs the ordinary copy in a larger function. + * versioned when that analysis proves what the function's own analysis + * leaves unproven: both operands of a remainder, or the index of a typed + * array read or write. Only those qualify: the operands of a product or a + * sum of such locals are as often fractional, while an index that is not + * an integer reads undefined, and a loop whose test fails runs the + * ordinary copy in a larger function. A for loop qualifies only when its + * initializer writes none of the assumed locals, so that they hold their + * entry values when the test reads them. * The entry test covers only the assumed locals that such an operation reads, directly, * through the loop's writes of other locals, or through the loop condition; * when that drops any, the function is analyzed once more with the smaller @@ -58,19 +62,21 @@ export function findLoopEntryVersions( .map((l) => l.id), ); const known = (e: IrExpr): boolean => !!facts.ranges.get(e); - let candidates = new Map>(); + // Keyed by the statement (a while or for loop). + let candidates = new Map>(); everyStmtList(fn.body, { expr: () => true, stmt: (s) => { - if (s.kind !== "while" || containsLoop(s.body)) return true; - const assumed = entryCandidates(s, eligible, known); - if (assumed.size > 0) candidates.set(s, assumed); + const loop: IrStmt = s; + if ((s.kind !== "while" && s.kind !== "for") || containsLoop(s.body)) return true; + const assumed = entryCandidates(loop, eligible, known); + if (assumed.size > 0) candidates.set(loop, assumed); return true; }, }); if (candidates.size === 0) return result; let versioned = analyzeIntegerRangeFacts(fn, slots, assumptionsOf(candidates)); - const pruned = new Map>(); + const pruned = new Map>(); let dropped = false; for (const [loop, entry] of candidates) { const roots = gainedOperands(loop, known, versioned); @@ -104,7 +110,7 @@ export function findLoopEntryVersions( } function assumptionsOf( - candidates: ReadonlyMap>, + candidates: ReadonlyMap>, ): LoopEntryAssumptions { const result = new Map>(); for (const [loop, entry] of candidates) result.set(loop, entry); @@ -112,9 +118,10 @@ function assumptionsOf( } /** Number locals the loop reads without a range fact and does not declare, - * when the loop has a remainder without one. */ + * when the loop has a remainder or a typed array index without one. A for + * loop whose initializer writes such a local has none. */ function entryCandidates( - loop: WhileLoop, + loop: IrStmt, eligible: ReadonlySet, known: (e: IrExpr) => boolean, ): Map { @@ -123,29 +130,64 @@ function entryCandidates( let integerWork = false; everyStmt(loop, { expr: (e) => { - if (e.kind === "varRef" && eligible.has(e.localId) && !known(e)) read.add(e.localId); + if ((e.kind === "varRef" || e.kind === "incDec") && eligible.has(e.localId) && !known(e)) + read.add(e.localId); else if (e.kind === "bin" && e.op === "%" && !known(e)) integerWork = true; + const index = bytesIndex(e); + if (index && !known(index)) integerWork = true; return true; }, stmt: (s) => { if (s.kind === "varDecl") declared.add(s.localId); + if (s.kind === "bytesSet" && !known(s.index)) integerWork = true; return true; }, }); const result = new Map(); if (!integerWork) return result; - for (const id of read) if (!declared.has(id)) result.set(id, LOOP_ENTRY_RANGE); + const initialized = loop.kind === "for" && loop.init ? writtenIn(loop.init) : new Set(); + for (const id of read) { + if (initialized.has(id)) return new Map(); + if (!declared.has(id)) result.set(id, LOOP_ENTRY_RANGE); + } return result; } -/** Operands of the loop's remainders that the versioned analysis proves - * where the function's own analysis does not prove both. */ +/** The index of a typed array element read (`a[i]`, `buf.readInt16LE(i)`). */ +function bytesIndex(e: IrExpr): IrExpr | undefined { + if (e.kind !== "bytesIntrinsic") return undefined; + const numeric = byteNumberAccess(e); + if (numeric) return e.args[numeric.offsetArg]; + return e.method === "get" ? e.args[0] : undefined; +} + +/** The locals `node` assigns, declares or increments. */ +function writtenIn(node: IrStmt): Set { + const ids = new Set(); + everyStmt(node, { + expr: (e) => { + if (e.kind === "assignExpr" || e.kind === "incDec") ids.add(e.localId); + return true; + }, + stmt: (s) => { + if (s.kind === "assign" || s.kind === "varDecl") ids.add(s.localId); + return true; + }, + }); + return ids; +} + +/** Operands of the loop's remainders and typed array indexes that the + * versioned analysis proves where the function's own analysis does not + * prove them. */ function gainedOperands( - loop: WhileLoop, + loop: IrStmt, known: (e: IrExpr) => boolean, versioned: IntegerRangeFacts, ): IrExpr[] { const operands: IrExpr[] = []; + const gained = (e: IrExpr | undefined): boolean => + e !== undefined && !known(e) && !!versioned.ranges.get(e); everyStmt(loop, { expr: (e) => { if ( @@ -156,23 +198,32 @@ function gainedOperands( !!versioned.ranges.get(e.right) ) operands.push(e.left, e.right); + const index = bytesIndex(e); + if (gained(index)) operands.push(index!); + return true; + }, + stmt: (s) => { + if (s.kind === "bytesSet" && gained(s.index)) operands.push(s.index); return true; }, - stmt: () => true, }); return operands; } /** The locals `roots` read, closed over the loop's writes (a local written * from others reads them) and over its condition (a local the condition - * reads makes every local of the condition relevant). */ -function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set { + * reads makes every local of the condition relevant). A for loop's + * condition bounds its passes, and with them the positions its body + * advances (`o++` in a loop of `line` passes), so its locals are always + * relevant. An increment or assignment reads its own local. */ +function relevantLocals(loop: IrStmt, roots: readonly IrExpr[]): Set { const relevant = new Set(); - const readsOf = (e: IrExpr): string[] => { + const readsOf = (e: IrExpr | null | undefined): string[] => { const ids: string[] = []; + if (!e) return ids; everyExpr(e, { expr: (x) => { - if (x.kind === "varRef") ids.push(x.localId); + if (x.kind === "varRef" || x.kind === "incDec") ids.push(x.localId); return true; }, stmt: () => true, @@ -193,7 +244,7 @@ function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set return true; }, }); - const condition = readsOf(loop.cond); + const condition = readsOf(loop.kind === "while" || loop.kind === "for" ? loop.cond : null); const pending: string[] = []; const add = (id: string): void => { if (relevant.has(id)) return; @@ -201,6 +252,7 @@ function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set pending.push(id); }; for (const root of roots) for (const id of readsOf(root)) add(id); + if (loop.kind === "for") for (const id of condition) add(id); while (pending.length > 0) { const id = pending.pop()!; for (const value of written.get(id) ?? []) for (const other of readsOf(value)) add(other); diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts index fccf069731..69b413b553 100644 --- a/packages/compiler/test/int32-element-array-emission.test.ts +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -191,8 +191,10 @@ test("a loop over a counter array tests its kind once on entry", async () => { /icmp eq i8 %\w+, 7\n\s+br i1 %\w+, label %kind\.fast\d+, label %kind\.wide/, ); const rest = counter.slice(counter.search(/^kind\.fast\d+:/m)); - // The fast copy is the last one, before the loop's exit. - const fast = rest.slice(0, rest.search(/^loop\.e\d+:/m)); + // The fast copy is the last one, before the loop's exit or, when the + // loop is also versioned on `n`'s entry range, before the copy for other + // entry values. + const fast = rest.slice(0, rest.search(/^(loop\.e|entry\.slow)\d+:/m)); expect(fast).not.toBe(""); expect(fast).not.toMatch(/icmp eq i8 %\w+, 7/); expect(fast).toMatch(/add nsw i64 %\w+, 1/); diff --git a/packages/compiler/test/loop-entry-versions.test.ts b/packages/compiler/test/loop-entry-versions.test.ts index 22e41e3e77..78d86a3bb0 100644 --- a/packages/compiler/test/loop-entry-versions.test.ts +++ b/packages/compiler/test/loop-entry-versions.test.ts @@ -43,3 +43,33 @@ test("while loops test their entry values and run integer remainders in the fast await rm(dir, { recursive: true, force: true }); } }); + +test("for loops test the positions they advance and step them as integers in the fast copy", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-entry-cursors-")); + try { + const entry = join(import.meta.dirname, "../../../tests/corpus/for-entry-range-cursors.ts"); + const outPath = join(dir, "cursors.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + const ll = emitLlvmModule(mod, { pointerBits: 64 }); + const body = (name: string): string => + ll.match( + new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\{([\\s\\S]*?)\\n\\}`), + )![1]!; + for (const name of ["lines", "reverse", "find"]) { + const fn = body(name); + const fast = fn.slice(fn.indexOf("\nentry.fast"), fn.indexOf("\nentry.slow")); + const slow = fn.slice(fn.indexOf("\nentry.slow")); + // The fast copy steps i64 shadows of the positions; the ordinary copy + // steps their doubles. + expect(fast, name).toMatch(/= (add|sub) nsw i64 %t\d+, 1\n/); + expect(fn, name).toMatch(/alloca i64 ; integer [io]\n/); + expect(slow, name).toMatch(/= f(add|sub) double %t\d+, 0x3FF0000000000000\n/); + } + // The initializer writes a position, so the loop has one copy. + expect(body("restart")).not.toContain("entry.fast"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-entry-range-cursors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-entry-range-cursors.ts.json new file mode 100644 index 0000000000..3dcf49574a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-entry-range-cursors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/for-entry-range-cursors.ts" + ], + "diags": [] +} diff --git a/tests/corpus/for-entry-range-cursors.ts b/tests/corpus/for-entry-range-cursors.ts new file mode 100644 index 0000000000..71599277fd --- /dev/null +++ b/tests/corpus/for-entry-range-cursors.ts @@ -0,0 +1,116 @@ +// For loops that advance positions carried in from outside the loop +// (`out[o++] = f(src[i++])` in a loop of `line` passes). Entry positions and +// pass counts cover integers inside and outside [-2^48, 2^48], fractions, +// -0, NaN and infinities. + +function code(rand: number): number { + if (rand < 0.25) return 97; + if (rand < 0.5) return 99; + if (rand < 0.75) return 103; + return 116; +} + +// Lines of `line` codes, each followed by a newline, from `start` on. +function lines(input: Float64Array, output: Uint8Array, count: number, start: number): number { + let i = start; + let o = start; + while (count > 0) { + const line = Math.min(count, 6); + for (let j = 0; j < line; j++) output[o++] = code(input[i++]!); + output[o++] = 10; + count -= line; + } + return o + i / 1000; +} + +// Writes backwards from `o`, reading forwards from `i`, with prefix steps. +function reverse(input: Float64Array, output: Uint8Array, line: number, i: number, o: number): string { + for (let j = 0; j < line; j++) output[--o] = code(input[++i]!); + return `${i} ${o}`; +} + +// Leaves the loop with a return that reads both positions. +function find(input: Float64Array, line: number, i: number, o: number): number { + for (let j = 0; j < line; j++) { + if (input[i++]! > 0.9) return i * 1000 + o; + o++; + } + return -(i * 1000 + o); +} + +// The initializer writes a carried position. +function restart(input: Float64Array, output: Uint8Array, line: number, o: number): number { + let i = 3; + for (let j = (i = 0); j < line; j++) output[o++] = code(input[i++]!); + return i * 100 + o; +} + +// A callee that throws partway through the loop. +function guarded(input: Float64Array, output: Uint8Array, line: number, i: number): number { + let o = 0; + for (let j = 0; j < line; j++) { + const rand = input[i++]!; + if (rand > 0.91) throw new RangeError(`stop at ${i} ${o}`); + output[o++] = code(rand); + } + return o; +} + +const input = new Float64Array(64); +let seed = 42; +for (let k = 0; k < input.length; k++) { + seed = (seed * 3877 + 29573) % 139968; + input[k] = seed / 139968; +} +function show(bytes: Uint8Array): string { + let text = ""; + for (const b of bytes) text += b === 10 ? "|" : b === 0 ? "." : String.fromCharCode(b); + return text; +} + +const starts = [0, 3, -0, -2, 2.5, NaN, Infinity, -Infinity, 2 ** 48, 2 ** 48 + 2, 2 ** 53, 60]; +for (const start of starts) { + const output = new Uint8Array(40); + const end = lines(input, output, 20, start); + console.log("lines", start, end, show(output)); +} +for (const count of [0, 1, 6, 7, 2.5, -3, NaN]) { + const output = new Uint8Array(24); + console.log("count", count, lines(input, output, count, 0), show(output)); +} +for (const [line, i, o] of [ + [5, 0, 10], + [5, -1, 4], + [3.5, 2, 8], + [NaN, 0, 5], + [4, 0.5, 6], + [4, 2 ** 48, 2 ** 48], + [4, 1, -0], +] as const) { + const output = new Uint8Array(12); + console.log("reverse", line, i, o, reverse(input, output, line, i, o), show(output)); +} +for (const [line, i, o] of [ + [10, 0, 0], + [64, 5, -3], + [3, 2 ** 48 - 1, 7], + [8, 1.5, 0], +] as const) { + console.log("find", line, i, o, find(input, line, i, o)); +} +for (const [line, o] of [ + [4, 0], + [4, 2], + [2, NaN], +] as const) { + const output = new Uint8Array(8); + console.log("restart", line, o, restart(input, output, line, o), show(output)); +} +for (const start of [0, 20, 40, 2.5]) { + const output = new Uint8Array(64); + try { + console.log("guarded", start, guarded(input, output, 30, start), show(output)); + } catch (error) { + console.log("guarded", start, error instanceof RangeError, (error as Error).message); + } +} diff --git a/tests/harness/corpus-costs/for-entry-range-cursors.ts.json b/tests/harness/corpus-costs/for-entry-range-cursors.ts.json new file mode 100644 index 0000000000..ec5830b998 --- /dev/null +++ b/tests/harness/corpus-costs/for-entry-range-cursors.ts.json @@ -0,0 +1 @@ +{ "plain": 5800, "san": 6664 }