mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Run two-counter loops and element copies over number arrays without per-element checks
This commit is contained in:
@@ -505,8 +505,12 @@ export function emitDenseArrayState(
|
||||
indexExpr: IrExpr | undefined,
|
||||
kind: "state" | "has",
|
||||
width: number,
|
||||
/** The index is a dense array loop counter its entry test proved below
|
||||
* the hole-free prefix (provenDenseIndex): the slot is a present value. */
|
||||
proven = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
if (proven) return kind === "state" ? f64Lit(SCR_ARR_VALUE) : "true";
|
||||
const fn = kind === "state" ? "scr_arr_state" : "scr_arr_has";
|
||||
const ty = kind === "state" ? "double" : "i1";
|
||||
host.declare(`declare ${kind === "state" ? "double" : "zeroext i1"} @${fn}(ptr, double)`);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
matchDenseArrayLoop,
|
||||
pairedDenseShape,
|
||||
type DenseLoopShape,
|
||||
} from "./dense-array-loops.js";
|
||||
|
||||
@@ -42,6 +43,8 @@ const locals = new Map<string, IrLocal>([
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: true }],
|
||||
["c", { id: "c", name: "c", type: ARR, mutable: false }],
|
||||
["j", { id: "j", name: "j", type: F64, mutable: true }],
|
||||
["t", { id: "t", name: "t", type: F64, mutable: false }],
|
||||
]);
|
||||
|
||||
/** for (let i = 0; i < a.length; i++) body */
|
||||
@@ -193,3 +196,94 @@ test("a loop to another invariant limit covers each array whose length it keeps"
|
||||
expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]);
|
||||
expect(matchLimited(resizing)).toBeNull();
|
||||
});
|
||||
|
||||
test("a two-counter loop covers the arrays indexed at either counter", () => {
|
||||
// for (let i = 1, j = k - 1; i < j; i++, j--) { t = a[i]; a[i] = a[j]; a[j] = t; }
|
||||
const paired: IntegerPairedForLoop = {
|
||||
low: { localId: "i", start: num(1), step: 1, startGuarded: false },
|
||||
high: { localId: "j", start: ref("k"), step: -1, startGuarded: true },
|
||||
inclusive: false,
|
||||
guarded: true,
|
||||
range: { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
|
||||
};
|
||||
const pairedShape = pairedDenseShape(paired);
|
||||
const matchPaired = (body: IrStmt[]) =>
|
||||
matchDenseArrayLoop(loop(body), pairedShape, locals, new Set(), new Set(["a", "b"]));
|
||||
const swap: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: "t", init: read("a"), loc },
|
||||
write("a", read("a", ref("j"))),
|
||||
write("a", ref("t"), ref("j")),
|
||||
];
|
||||
const dense = matchPaired(swap);
|
||||
expect([...dense!.arrays.keys()]).toEqual(["a"]);
|
||||
expect(dense!.pair).toEqual({ high: "j", lowStart: num(1), highStart: ref("k") });
|
||||
expect(dense!.range).toEqual({ min: 0, max: 2147483646 });
|
||||
// Stores at any other index may extend the array.
|
||||
expect(matchPaired([...swap, write("a", num(0), ref("k"))])).toBeNull();
|
||||
});
|
||||
|
||||
test("a present-undefined store is covered only where it stores a covered read's missing arm", () => {
|
||||
// const v = b[i] (as number | undefined);
|
||||
// if (v is undefined) c[i] = undefined; else c[i] = v is number ? v : throw;
|
||||
const unionType = { kind: "union" as const, unionId: "u" };
|
||||
const v: IrExpr = { kind: "varRef", localId: "v", type: unionType, loc };
|
||||
const isTag = (tag: number): IrExpr => ({
|
||||
kind: "unionIsTag",
|
||||
unionId: "u",
|
||||
tag,
|
||||
negated: false,
|
||||
value: v,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const reader = (array: string, index = ref("i")): IrExpr => ({
|
||||
kind: "call",
|
||||
callee: "%arr.idxOr.f64",
|
||||
args: [arr(array), index],
|
||||
type: unionType,
|
||||
loc,
|
||||
});
|
||||
const undefinedStore: IrStmt = { kind: "arraySetUndefined", arr: arr("c"), index: ref("i"), loc };
|
||||
const guarded = (array: string): IrStmt[] => [
|
||||
{ kind: "varDecl", localId: "v", init: reader(array), loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: isTag(1),
|
||||
then: [undefinedStore],
|
||||
else_: [
|
||||
write("c", {
|
||||
kind: "ternary",
|
||||
cond: isTag(0),
|
||||
then: { kind: "unionNarrow", unionId: "u", tag: 0, value: v, type: F64, loc },
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [num(1), { kind: "strLit", value: "", type: { kind: "string" }, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
],
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const withV = new Map([
|
||||
...locals,
|
||||
["v", { id: "v", name: "v", type: unionType, mutable: false }],
|
||||
]);
|
||||
const matchStores = (body: IrStmt[], initialized: string[]) =>
|
||||
matchDenseArrayLoop(loop(body), shape, withV, new Set(), new Set(initialized));
|
||||
const dense = matchStores(guarded("b"), ["a", "b", "c"]);
|
||||
expect([...dense!.arrays.keys()].sort()).toEqual(["b", "c"]);
|
||||
// A read of an array the entry test does not cover may miss in the fast
|
||||
// copy, and its undefined store would run there.
|
||||
expect(matchStores(guarded("b"), ["a", "c"])).toBeNull();
|
||||
// A present undefined stored on its own leaves its array out.
|
||||
const alone = matchStores(
|
||||
[{ kind: "exprStmt", expr: read("b"), loc }, undefinedStore],
|
||||
["a", "b", "c"],
|
||||
);
|
||||
expect([...alone!.arrays.keys()]).toEqual(["b"]);
|
||||
});
|
||||
|
||||
@@ -20,16 +20,24 @@
|
||||
* A counted loop with another invariant limit, such as `j < n` after
|
||||
* `const n = a.length` or `j < 5`, is covered the same way: the test bounds
|
||||
* the loop's exclusive limit by 2^31 - 1 and by each indexed array's
|
||||
* `filled`, and the loop must provably never change any of their lengths. */
|
||||
* `filled`, and the loop must provably never change any of their lengths.
|
||||
*
|
||||
* A two-counter loop closing in on itself, such as
|
||||
* `for (let i = 1, j = k - 1; i < j; i++, j--)`, indexes only between its
|
||||
* low counter's start and its high counter's: the low counter only rises
|
||||
* and stays below the high one, which only falls. Its test bounds the low
|
||||
* start below by 0 and the high start by 2^31 - 2 and below each indexed
|
||||
* array's `filled`, both starts being exact integers. */
|
||||
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import { preservesArrayLength } from "../../ir/array-length-stability.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
|
||||
import { emitCountedLoopLimit } from "./counted-loops.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
@@ -47,6 +55,17 @@ export interface DenseArrayLoop {
|
||||
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
|
||||
/** The counter's range on every pass of the fast copy. */
|
||||
range: IntegerRange;
|
||||
/** A two-counter loop's high counter and both starts, when the counter
|
||||
* is its low one (`length` and `limit` are then null). */
|
||||
pair: DensePair | null;
|
||||
}
|
||||
|
||||
/** The high counter of a two-counter dense loop, and the starts its entry
|
||||
* test bounds. */
|
||||
export interface DensePair {
|
||||
high: string;
|
||||
lowStart: IrExpr;
|
||||
highStart: IrExpr;
|
||||
}
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
@@ -62,6 +81,8 @@ export interface DenseLoopShape {
|
||||
/** The counter's proven range on every pass of the ordinary loop; the
|
||||
* fast copy keeps its minimum, which must be at least 0. */
|
||||
range: IntegerRange;
|
||||
/** A two-counter loop's high counter and starts. */
|
||||
pair?: DensePair;
|
||||
}
|
||||
|
||||
/** `length.length - c` (c an integer literal, 0 when absent). */
|
||||
@@ -107,6 +128,124 @@ export function countedDenseShape(
|
||||
return { counter: counted.localId, length: start.receiver, range: counted.range };
|
||||
}
|
||||
|
||||
/** The shape of a two-counter loop whose low counter rises and high
|
||||
* counter falls: both stay between the two starts, which the entry test
|
||||
* bounds, so the fast copy's counters lie in [0, 2^31 - 2]. */
|
||||
export function pairedDenseShape(loop: IntegerPairedForLoop): DenseLoopShape {
|
||||
return {
|
||||
counter: loop.low.localId,
|
||||
length: null,
|
||||
range: { min: 0, max: INT32_MAX - 1 },
|
||||
pair: { high: loop.high.localId, lowStart: loop.low.start, highStart: loop.high.start },
|
||||
};
|
||||
}
|
||||
|
||||
interface ElementRead {
|
||||
arr: IrExpr;
|
||||
index: IrExpr;
|
||||
}
|
||||
|
||||
/** The array and index of a call of an element-reader helper
|
||||
* (`%arr.idxOr.*`, ir/int32-arrays.ts isElementReader): `a[i]` as
|
||||
* `number | undefined`. */
|
||||
function elementReaderCall(e: IrExpr): ElementRead | null {
|
||||
if (e.kind !== "call" || !e.callee.startsWith("%arr.idxOr.") || e.args.length !== 2) return null;
|
||||
return { arr: e.args[0]!, index: e.args[1]! };
|
||||
}
|
||||
|
||||
/** The array and index of an element read as `T | undefined`: an
|
||||
* element-reader call, or its body inlined for an int32 array
|
||||
* (`arrayState(a, i) === 1 ? wrap(a[i]) : wrap(undefined)`). */
|
||||
function elementRead(e: IrExpr): ElementRead | null {
|
||||
const call = elementReaderCall(e);
|
||||
if (call) return call;
|
||||
if (e.kind !== "ternary") return null;
|
||||
const cond = e.cond;
|
||||
if (cond.kind !== "bin" || cond.op !== "===") return null;
|
||||
const state = cond.left,
|
||||
present = cond.right,
|
||||
wrapped = e.then,
|
||||
missing = e.else_;
|
||||
if (state.kind !== "arrayState" || present.kind !== "numLit" || present.value !== 1) return null;
|
||||
if (wrapped.kind !== "unionWrap" || missing.kind !== "unionWrap") return null;
|
||||
const value = wrapped.value;
|
||||
if (value.kind !== "arrayGet" || missing.value.kind !== "unitLit") return null;
|
||||
const same = (x: IrExpr, y: IrExpr): boolean =>
|
||||
x.kind === "varRef" && y.kind === "varRef" && x.localId === y.localId;
|
||||
return same(state.arr, value.arr) && same(state.index, value.index)
|
||||
? { arr: state.arr, index: state.index }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The present-undefined stores of `a[i] = b[j]!` as lowering spells it,
|
||||
* with the element read each one stores the missing arm of:
|
||||
*
|
||||
* const v = %arr.idxOr(b, j);
|
||||
* if (v is undefined) a[i] = undefined; else a[i] = v is number ? v : throw;
|
||||
*
|
||||
* The read's local is declared once in the same list as the `if`, and only
|
||||
* read after it. */
|
||||
function missingElementStores(body: IrStmt[]): Map<IrStmt, ElementRead> {
|
||||
const result = new Map<IrStmt, ElementRead>();
|
||||
const writes = new Map<string, number>();
|
||||
const count = (id: string): void => void writes.set(id, (writes.get(id) ?? 0) + 1);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") count(e.localId);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") count(s.localId);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
body.every(stmt);
|
||||
const list = (stmts: IrStmt[]): void => {
|
||||
const reads = new Map<string, ElementRead>();
|
||||
for (const s of stmts) {
|
||||
if (s.kind === "varDecl" && s.init && writes.get(s.localId) === 1) {
|
||||
const read = elementRead(s.init);
|
||||
if (read) reads.set(s.localId, read);
|
||||
}
|
||||
if (s.kind === "if") {
|
||||
const store = missingArmStore(s, reads);
|
||||
if (store) result.set(store.stmt, store.read);
|
||||
}
|
||||
// Nested loops keep a loop out of the fast copy (matchDenseArrayLoop),
|
||||
// and other statement lists leave their stores unmatched.
|
||||
if (s.kind === "block") list(s.body);
|
||||
if (s.kind === "if") {
|
||||
list(s.then);
|
||||
if (s.else_) list(s.else_);
|
||||
}
|
||||
}
|
||||
};
|
||||
list(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The present-undefined store of `if (v is undefined) a[i] = undefined;
|
||||
* else a[i] = v is number ? v : throw;`, with the read `v` holds. */
|
||||
function missingArmStore(
|
||||
s: IrStmt & { kind: "if" },
|
||||
reads: ReadonlyMap<string, ElementRead>,
|
||||
): { stmt: IrStmt; read: ElementRead } | null {
|
||||
const cond = s.cond;
|
||||
if (cond.kind !== "unionIsTag" || cond.negated || cond.value.kind !== "varRef") return null;
|
||||
const local = cond.value.localId;
|
||||
const read = reads.get(local);
|
||||
if (!read || s.then.length !== 1 || s.else_?.length !== 1) return null;
|
||||
const store = s.then[0]!,
|
||||
other = s.else_[0]!;
|
||||
if (store.kind !== "arraySetUndefined" || other.kind !== "arraySet") return null;
|
||||
const value = other.value;
|
||||
if (value.kind !== "ternary") return null;
|
||||
const test = value.cond,
|
||||
narrowed = value.then;
|
||||
if (test.kind !== "unionIsTag" || test.negated || test.tag === cond.tag) return null;
|
||||
if (test.value.kind !== "varRef" || test.value.localId !== local) return null;
|
||||
if (narrowed.kind !== "unionNarrow" || narrowed.tag !== test.tag) return null;
|
||||
return narrowed.type.kind === "f64" ? { stmt: store, read } : null;
|
||||
}
|
||||
|
||||
/** Match the fast copy of a loop of the given shape. `initialized` holds the
|
||||
* bindings definitely assigned when the loop starts; `unaliased` the local
|
||||
* arrays no other binding holds. */
|
||||
@@ -118,6 +257,8 @@ export function matchDenseArrayLoop(
|
||||
initialized: ReadonlySet<string>,
|
||||
): DenseArrayLoop | null {
|
||||
const { counter, length } = shape;
|
||||
const high = shape.pair?.high ?? null;
|
||||
const isCounter = (id: string): boolean => id === counter || id === high;
|
||||
if (shape.range.min < 0 || !loop.cond) return null;
|
||||
let nodes = 0,
|
||||
nested = false;
|
||||
@@ -126,13 +267,18 @@ export function matchDenseArrayLoop(
|
||||
// element write.
|
||||
const disturbed = new Set<string>();
|
||||
const access = (arr: IrExpr, index: IrExpr): void => {
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter)
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && isCounter(index.localId))
|
||||
candidates.set(arr.localId, arr);
|
||||
};
|
||||
const missing = missingElementStores(loop.body);
|
||||
// The element reads behind the guarded present-undefined stores.
|
||||
const guards: ElementRead[] = [];
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId);
|
||||
if (e.kind === "arrayGet") access(e.arr, e.index);
|
||||
const reader = elementReaderCall(e);
|
||||
if (reader) access(reader.arr, reader.index);
|
||||
if (e.kind === "arrIntrinsic") {
|
||||
if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]);
|
||||
else if (e.method !== "length" && e.receiver.kind === "varRef")
|
||||
@@ -147,9 +293,21 @@ export function matchDenseArrayLoop(
|
||||
if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId);
|
||||
if (s.kind === "arraySet") {
|
||||
access(s.arr, s.index);
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter))
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || !isCounter(s.index.localId)))
|
||||
disturbed.add(s.arr.localId);
|
||||
}
|
||||
if (s.kind === "arraySetUndefined") {
|
||||
// A present undefined at a counter only where it stores a missing
|
||||
// element read (`a[i] = b[j]!`): the fast copy reads a present value
|
||||
// there, so the store never runs in it.
|
||||
const read = missing.get(s);
|
||||
if (read === undefined || s.index.kind !== "varRef" || !isCounter(s.index.localId)) {
|
||||
if (s.arr.kind === "varRef") disturbed.add(s.arr.localId);
|
||||
} else {
|
||||
access(s.arr, s.index);
|
||||
guards.push(read);
|
||||
}
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
|
||||
@@ -180,18 +338,33 @@ export function matchDenseArrayLoop(
|
||||
// once: the loop must not change any of their lengths.
|
||||
if (
|
||||
length === null &&
|
||||
!preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased)
|
||||
!preservesArrayLength(
|
||||
nodesOfLoop,
|
||||
{ receiver: id, inRange: counter, inRangeHigh: high },
|
||||
locals,
|
||||
unaliased,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
arrays.set(id, ref);
|
||||
}
|
||||
if (arrays.size === 0) return null;
|
||||
// Each guarded store's read must be one the fast copy proves present.
|
||||
for (const { arr, index } of guards)
|
||||
if (
|
||||
arr.kind !== "varRef" ||
|
||||
!arrays.has(arr.localId) ||
|
||||
index.kind !== "varRef" ||
|
||||
!isCounter(index.localId)
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
counter,
|
||||
length,
|
||||
limit: length === null ? (shape.limit ?? null) : null,
|
||||
arrays,
|
||||
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
|
||||
pair: shape.pair ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,6 +380,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
|
||||
host.markMemoryPointer(ptr, "array:header");
|
||||
return ptr;
|
||||
};
|
||||
if (loop.pair !== null) return emitPairedDenseGuard(host, loop, loop.pair, header);
|
||||
if (loop.length === null) {
|
||||
// A limit at or below the start runs no pass, and passes the test.
|
||||
const limit = emitCountedLoopLimit(host, loop.limit!);
|
||||
@@ -254,3 +428,54 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
/** A two-counter loop's entry test: both starts are exact integers, the
|
||||
* low one at least 0 and the high one at most 2^31 - 2 and below every
|
||||
* indexed array's hole-free prefix bound. The starts are stable values
|
||||
* (matchIntegerPairedForLoop), so evaluating them here again is unobservable. */
|
||||
function emitPairedDenseGuard(
|
||||
host: LlvmEmitterContext,
|
||||
loop: DenseArrayLoop,
|
||||
pair: DensePair,
|
||||
header: (array: string, field: number) => string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const integer = (expr: IrExpr, min: number, max: number): { value: string; ok: string } => {
|
||||
const start = host.emitExpr(expr);
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
value = B.tmp(),
|
||||
back = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
inRange = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(min)}`);
|
||||
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(max)}`);
|
||||
B.line(`${inRange} = and i1 ${low}, ${high}`);
|
||||
B.line(`${raw} = fptosi double ${start.name} to i64`);
|
||||
B.line(`${value} = freeze i64 ${raw}`);
|
||||
B.line(`${back} = sitofp i64 ${value} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${back}, ${start.name}`);
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
return { value, ok };
|
||||
};
|
||||
const low = integer(pair.lowStart, 0, INT32_MAX - 1);
|
||||
const high = integer(pair.highStart, -1, INT32_MAX - 1);
|
||||
let valid = B.tmp();
|
||||
B.line(`${valid} = and i1 ${low.ok}, ${high.ok}`);
|
||||
for (const ref of loop.arrays.values()) {
|
||||
const array = host.emitStableReceiver(ref, []);
|
||||
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
|
||||
filled = B.tmp(),
|
||||
wide = B.tmp(),
|
||||
covered = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
B.line(`${wide} = zext i32 ${filled} to i64`);
|
||||
B.line(`${covered} = icmp slt i64 ${high.value}, ${wide}`);
|
||||
B.line(`${both} = and i1 ${valid}, ${covered}`);
|
||||
valid = both;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,12 @@ import {
|
||||
matchIntegerPairedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
|
||||
import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
emitDenseLoopGuard,
|
||||
matchDenseArrayLoop,
|
||||
pairedDenseShape,
|
||||
} from "./dense-array-loops.js";
|
||||
import {
|
||||
emitCountedLoopLimit,
|
||||
emitCountedLoopSafety,
|
||||
@@ -966,7 +971,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
: pairedLoop
|
||||
? pairedDenseShape(pairedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
denseShape && !window
|
||||
? matchDenseArrayLoop(
|
||||
@@ -1144,11 +1151,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`${start} = fptosi double ${value.name} to i64`);
|
||||
B.line(`store i64 ${start}, ptr ${slot}`);
|
||||
pairedSlots.push(slot);
|
||||
const dense = LOOPS.of(emitter).denseLoop;
|
||||
emitter.integerLoopBindings.set(counter.localId, {
|
||||
slot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: pairedLoop.range,
|
||||
range:
|
||||
dense?.pair && dense.counter === pairedLoop.low.localId
|
||||
? dense.range
|
||||
: pairedLoop.range,
|
||||
});
|
||||
}
|
||||
} else if (s.init) {
|
||||
|
||||
@@ -974,8 +974,8 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
* prefix: a counter of a dense array loop's fast copy indexing one of the
|
||||
* arrays its entry test covered. */
|
||||
provenDenseIndex(arr: IrExpr, index: IrExpr): boolean {
|
||||
const loop = LOOPS.of(this).denseLoop;
|
||||
return (
|
||||
@@ -983,8 +983,8 @@ export class LlEmitter {
|
||||
arr.kind === "varRef" &&
|
||||
loop.arrays.has(arr.localId) &&
|
||||
index.kind === "varRef" &&
|
||||
index.localId === loop.counter &&
|
||||
this.integerLoopBindings.has(loop.counter)
|
||||
(index.localId === loop.counter || index.localId === loop.pair?.high) &&
|
||||
this.integerLoopBindings.has(index.localId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1076,9 +1076,10 @@ export function emitContainerExpr(
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
|
||||
const proven = host.provenDenseIndex(e.arr, e.index);
|
||||
const t = host.checkedInt32Array(e.arr)
|
||||
? host.emitByElementKind(arr.name, "double", (kind) =>
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)),
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind), proven),
|
||||
)!
|
||||
: emitDenseArrayState(
|
||||
host,
|
||||
@@ -1087,6 +1088,7 @@ export function emitContainerExpr(
|
||||
e.index,
|
||||
"state",
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
proven,
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
@@ -797,7 +797,33 @@ export function emitLocalArrayRead(
|
||||
);
|
||||
return null;
|
||||
});
|
||||
else if (inline)
|
||||
else if (
|
||||
inline &&
|
||||
loopIndex !== null &&
|
||||
read.element.kind === "f64" &&
|
||||
host.provenDenseIndex(read.array, read.index)
|
||||
) {
|
||||
// In a dense array loop's fast copy, the entry test proved the counter
|
||||
// below the array's hole-free prefix: the slot holds a present value,
|
||||
// and the read has no missing arm.
|
||||
const dataPtr = B.tmp(),
|
||||
data = B.tmp(),
|
||||
valuePtr = B.tmp(),
|
||||
value = B.tmp(),
|
||||
width = int32 ? "i32" : "i64";
|
||||
B.line(
|
||||
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
|
||||
);
|
||||
host.markMemoryPointer(dataPtr, "array:header");
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
|
||||
B.line(
|
||||
`${valuePtr} = getelementptr inbounds ${width}, ptr ${data}, ${host.sizeType} ${loopIndex}`,
|
||||
);
|
||||
host.markMemoryPointer(valuePtr, "array:elements");
|
||||
B.line(`${value} = load ${width}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
storeValue(value);
|
||||
B.br(join);
|
||||
} else if (inline)
|
||||
emitDenseReferenceArrayRead(
|
||||
host,
|
||||
array,
|
||||
|
||||
@@ -193,3 +193,34 @@ test("number and boolean writes to class fields run no user code", () => {
|
||||
// A reference stored into a field releases the old one.
|
||||
expect(preserves([fieldSet(arr("b"))], "i", new Set(), scope)).toBe(false);
|
||||
});
|
||||
|
||||
test("optional element reads, their assertion errors and present-undefined stores at an in-range index keep the length", () => {
|
||||
const optional = (receiver: string, index: IrExpr): IrExpr => ({
|
||||
kind: "call",
|
||||
callee: "%arr.idxOr.f64",
|
||||
args: [arr(receiver), index],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const undefinedAt = (receiver: string, index: IrExpr): IrStmt => ({
|
||||
kind: "arraySetUndefined",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
loc,
|
||||
});
|
||||
const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [num(1)], type: F64, loc };
|
||||
expect(preserves([stmt(optional("a", ref("i"))), stmt(thrown)])).toBe(true);
|
||||
expect(preserves([undefinedAt("a", ref("i"))])).toBe(true);
|
||||
// Past the counter, a present undefined extends the array.
|
||||
expect(preserves([undefinedAt("a", plus(ref("i"), num(1)))])).toBe(false);
|
||||
// The high counter of a two-counter loop is in range as well.
|
||||
const paired = (body: IrStmt[]): boolean =>
|
||||
preservesArrayLength(
|
||||
[cond, ...body],
|
||||
{ receiver: "a", inRange: "i", inRangeHigh: "j" },
|
||||
locals,
|
||||
new Set(),
|
||||
);
|
||||
expect(paired([undefinedAt("a", ref("j")), set("a", ref("j"), num(0))])).toBe(true);
|
||||
expect(preserves([set("a", ref("j"), num(0))])).toBe(false);
|
||||
});
|
||||
|
||||
@@ -162,6 +162,10 @@ export interface ArrayLengthLoop {
|
||||
* long as the length itself has not changed). An element write at this
|
||||
* index replaces an existing element or sets a non-index property. */
|
||||
inRange: string | null;
|
||||
/** A second such binding: the high counter of a two-counter loop
|
||||
* (`for (let i = lo, j = hi; i < j; i++, j--)`), whose values stay
|
||||
* between the low counter's start and its own. */
|
||||
inRangeHigh?: string | null;
|
||||
}
|
||||
|
||||
/** Whether running `nodes` (a loop's condition, body and update) can never
|
||||
@@ -191,7 +195,10 @@ export function preservesArrayLength(
|
||||
// escaping.
|
||||
const receiver = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && e.localId === loop.receiver) || expr(e);
|
||||
const written = (id: string): boolean => id === loop.receiver || id === loop.inRange;
|
||||
const inRange = (id: string): boolean =>
|
||||
(loop.inRange !== null && id === loop.inRange) ||
|
||||
(loop.inRangeHigh != null && id === loop.inRangeHigh);
|
||||
const written = (id: string): boolean => id === loop.receiver || inRange(id);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
@@ -224,12 +231,21 @@ export function preservesArrayLength(
|
||||
case "call":
|
||||
// A checked union extraction (`x!`, coercions/unions.ts): the
|
||||
// asserted arm's payload or a TypeError, and no user code.
|
||||
return e.callee.startsWith("%union.narrow.") && e.args.every(expr);
|
||||
if (e.callee.startsWith("%union.narrow.")) return e.args.every(expr);
|
||||
// An element read as `T | undefined` (int32-arrays.ts isElementReader).
|
||||
return (
|
||||
e.callee.startsWith("%arr.idxOr.") &&
|
||||
e.args.length === 2 &&
|
||||
receiver(e.args[0]!) &&
|
||||
expr(e.args[1]!)
|
||||
);
|
||||
case "mapIntrinsic":
|
||||
return READING_MAP_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr);
|
||||
case "setIntrinsic":
|
||||
return READING_SET_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr);
|
||||
case "libCall":
|
||||
// The TypeError of an assertion past a union arm leaves the loop.
|
||||
if (e.fn === "error.nodeThrow") return e.args.every(expr);
|
||||
return (
|
||||
isBorrowSafeMath(e.fn) &&
|
||||
e.args.every((arg) => arg.type.kind === "f64") &&
|
||||
@@ -254,14 +270,18 @@ export function preservesArrayLength(
|
||||
break;
|
||||
case "arraySet":
|
||||
return (
|
||||
(separate(s.arr) ||
|
||||
(s.index.kind === "varRef" &&
|
||||
loop.inRange !== null &&
|
||||
s.index.localId === loop.inRange)) &&
|
||||
(separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) &&
|
||||
receiver(s.arr) &&
|
||||
expr(s.index) &&
|
||||
expr(s.value)
|
||||
);
|
||||
case "arraySetUndefined":
|
||||
// A present undefined replacing an element below the length.
|
||||
return (
|
||||
(separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) &&
|
||||
receiver(s.arr) &&
|
||||
expr(s.index)
|
||||
);
|
||||
case "return":
|
||||
return s.value === null || expr(s.value);
|
||||
case "fieldSet":
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7264-array-dense-paired-loops.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Two-counter loops that index number arrays at both counters
|
||||
// (`for (let i = lo, j = hi; i < j; i++, j--)`), and element copies through
|
||||
// `a[i]!` reads, both inside and outside the ranges a single entry test can
|
||||
// cover: filled prefixes, holes, present undefined, reads past the end,
|
||||
// fractional and negative starts, int32 arrays and long checked arrays.
|
||||
|
||||
function reversePrefix(a: number[], end: number): void {
|
||||
for (let i = 0, j = end; i < j; i++, j--) {
|
||||
const t = a[i]!;
|
||||
a[i] = a[j]!;
|
||||
a[j] = t;
|
||||
}
|
||||
}
|
||||
|
||||
function show(label: string, a: number[]): void {
|
||||
console.log(label, a.length, JSON.stringify(a), a);
|
||||
}
|
||||
|
||||
// The whole prefix is present: the entry test passes.
|
||||
const plain: number[] = [];
|
||||
for (let k = 0; k < 8; k++) plain[k] = k * 3;
|
||||
reversePrefix(plain, 5);
|
||||
show("plain", plain);
|
||||
reversePrefix(plain, 7);
|
||||
show("whole", plain);
|
||||
reversePrefix(plain, 0);
|
||||
show("empty range", plain);
|
||||
|
||||
// The high start is past the end: the reads there miss, the stores extend.
|
||||
const short = [1, 2, 3];
|
||||
reversePrefix(short, 5);
|
||||
show("past the end", short);
|
||||
|
||||
// A hole inside the range.
|
||||
const holey: number[] = [10, 20];
|
||||
holey[3] = 40;
|
||||
holey[4] = 50;
|
||||
reversePrefix(holey, 4);
|
||||
show("hole", holey);
|
||||
|
||||
// A present undefined inside the range, copied by a mirror loop.
|
||||
const loose: number[] = [7, 8, 9, 10];
|
||||
(loose as (number | undefined)[])[2] = undefined;
|
||||
for (let i = 0, j = 3; i < j; i++, j--) loose[i] = loose[j]!;
|
||||
show("undefined", loose);
|
||||
|
||||
// Fractional and negative starts index non-element properties.
|
||||
const fractional = [1, 2, 3, 4];
|
||||
reversePrefix(fractional, 2.5);
|
||||
show("fractional", fractional);
|
||||
const negative = [5, 6, 7];
|
||||
for (let i = -1, j = 2; i < j; i++, j--) negative[i] = negative[j]!;
|
||||
show("negative", negative);
|
||||
|
||||
// A local int32 array (every stored element is a small integer).
|
||||
function int32Flips(start: number): void {
|
||||
const counts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
let flips = 0;
|
||||
for (let first = counts[start]!; first !== 0; first = counts[0]!) {
|
||||
for (let i = 0, j = first; i < j; i++, j--) {
|
||||
const t = counts[i]!;
|
||||
counts[i] = counts[j]!;
|
||||
counts[j] = t;
|
||||
}
|
||||
flips++;
|
||||
if (flips > 20) break;
|
||||
}
|
||||
console.log("int32", flips, counts.join(","));
|
||||
}
|
||||
int32Flips(3);
|
||||
int32Flips(9);
|
||||
|
||||
// A local long array with checked int32 stores, widened by a fraction.
|
||||
function longChecked(n: number, fraction: number): void {
|
||||
const long = new Array<number>(n).fill(0);
|
||||
for (let k = 0; k < long.length; k++) long[k] = k;
|
||||
for (let i = 0, j = n - 1; i < j; i++, j--) {
|
||||
const t = long[i]!;
|
||||
long[i] = long[j]!;
|
||||
long[j] = t;
|
||||
}
|
||||
console.log("long", long[0], long[1], long[n - 2], long[n - 1]);
|
||||
long[10] = fraction;
|
||||
for (let i = 0, j = 20; i < j; i++, j--) {
|
||||
const t = long[i]!;
|
||||
long[i] = long[j]!;
|
||||
long[j] = t;
|
||||
}
|
||||
console.log("widened", long.slice(0, 12).join(","), long[20]);
|
||||
}
|
||||
longChecked(5000, 0.5);
|
||||
|
||||
// Element copies through `a[i]!`: the source is shorter than the range.
|
||||
function copy(to: number[], from: number[], n: number): void {
|
||||
for (let i = 0; i < n; i++) to[i] = from[i]!;
|
||||
}
|
||||
const into = [0, 0, 0, 0, 0, 0];
|
||||
copy(into, [1, 2, 3, 4, 5, 6], 6);
|
||||
show("copy", into);
|
||||
copy(into, [9, 8], 4);
|
||||
show("short source", into);
|
||||
const gappy: number[] = [1];
|
||||
gappy[2] = 3;
|
||||
gappy[3] = 4;
|
||||
copy(into, gappy, 4);
|
||||
show("gappy source", into);
|
||||
|
||||
// Two names for one array.
|
||||
function aliased(n: number): void {
|
||||
const same: number[] = [];
|
||||
for (let k = 0; k < n; k++) same[k] = k + 1;
|
||||
const alias = same;
|
||||
for (let i = 0, j = n - 1; i < j; i++, j--) {
|
||||
const t = alias[i]!;
|
||||
same[i] = alias[j]!;
|
||||
same[j] = t;
|
||||
}
|
||||
show("alias", same);
|
||||
}
|
||||
aliased(5);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2900, "san": 4200 }
|
||||
Reference in New Issue
Block a user