mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Version for loops on the positions they advance (#2415)
This commit is contained in:
@@ -2992,7 +2992,12 @@ function emitProvenLoop(
|
||||
}
|
||||
const deferred = new Set<string>();
|
||||
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);
|
||||
|
||||
@@ -1775,6 +1775,9 @@ export class LlEmitter {
|
||||
ran?: string;
|
||||
/** Writes that speculate a rewritten value (rewriteSpeculable). */
|
||||
values?: ReadonlyMap<IrStmt, SpeculativeValue>;
|
||||
/** A proven integer loop's expression-position `++`/`--` of its
|
||||
* shadowed bindings (ProvenLoopPlan.increments). */
|
||||
increments?: ReadonlySet<IrExpr>;
|
||||
} | null = null;
|
||||
/** Speculative loop sites numbered so far in the module. */
|
||||
speculativeLoopSites = 0;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<IrStmt, LoopEntryVersion> | null;
|
||||
/** Loops whose copies are being emitted: a loop is versioned once. */
|
||||
readonly versioning: Set<IrStmt>;
|
||||
@@ -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 (
|
||||
|
||||
@@ -773,6 +773,9 @@ export interface ProvenLoopPlan {
|
||||
locals: Map<string, IntegerRange>;
|
||||
/** Statement-level writes computed as integers. */
|
||||
writes: Set<IrStmt>;
|
||||
/** Expression-position `++`/`--` of shadowed bindings (`out[o++] = v`),
|
||||
* computed on the shadows. */
|
||||
increments: Set<IrExpr>;
|
||||
}
|
||||
|
||||
/** 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<string, IrStmt[]>();
|
||||
const increments = new Map<string, IrExpr[]>();
|
||||
const refused = new Set<string>();
|
||||
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<string, IntegerRange>();
|
||||
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<IrStmt>();
|
||||
for (const id of locals.keys()) for (const s of writes.get(id)!) planned.add(s);
|
||||
return { locals, writes: planned };
|
||||
const plannedIncrements = new Set<IrExpr>();
|
||||
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;
|
||||
|
||||
@@ -23,7 +23,8 @@ export interface IntegerRangeFacts {
|
||||
ranges: IntegerRanges;
|
||||
loopRanges: ReadonlyMap<IrStmt, ReadonlyMap<string, IntegerRange>>;
|
||||
}
|
||||
/** 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":
|
||||
|
||||
@@ -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<IrStmt, { kind: "bytesSet" }>).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<IrStmt, { kind: "bytesSet" }>).index)).toBeDefined();
|
||||
expect(
|
||||
version.ranges.get((read as Extract<IrExpr, { kind: "bytesIntrinsic" }>).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);
|
||||
});
|
||||
|
||||
@@ -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<IrStmt, { kind: "while" }>;
|
||||
|
||||
/** 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<string, IntegerRange> | 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<WhileLoop, ReadonlyMap<string, IntegerRange>>();
|
||||
// Keyed by the statement (a while or for loop).
|
||||
let candidates = new Map<IrStmt, ReadonlyMap<string, IntegerRange>>();
|
||||
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<WhileLoop, ReadonlyMap<string, IntegerRange>>();
|
||||
const pruned = new Map<IrStmt, ReadonlyMap<string, IntegerRange>>();
|
||||
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<WhileLoop, ReadonlyMap<string, IntegerRange>>,
|
||||
candidates: ReadonlyMap<IrStmt, ReadonlyMap<string, IntegerRange>>,
|
||||
): LoopEntryAssumptions {
|
||||
const result = new Map<IrStmt, ReadonlyMap<string, IntegerRange>>();
|
||||
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<string>,
|
||||
known: (e: IrExpr) => boolean,
|
||||
): Map<string, IntegerRange> {
|
||||
@@ -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<string, IntegerRange>();
|
||||
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<string>();
|
||||
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<string> {
|
||||
const ids = new Set<string>();
|
||||
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<string> {
|
||||
* 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<string> {
|
||||
const relevant = new Set<string>();
|
||||
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<string>
|
||||
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<string>
|
||||
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);
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/for-entry-range-cursors.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5800, "san": 6664 }
|
||||
Reference in New Issue
Block a user