mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Bound loop counters compared as i + c with a bound
The range analysis narrowed a counter only when the comparison read the counter itself (`i < n`). A comparison of the counter plus or minus an integer literal (`i + 1 < xs.length`, `1 + i < n`, `i - 1 >= 0`) now narrows it as `i < n - c` does. Against a bound within 2^52, a sum that rounds has a magnitude of at least 2^53, so every such comparison answers as the exact sum would. The counter of `for (let i = 0; i + 1 < xs.length; i++)` then lands in an i32 slot like the one of `i < xs.length - 1`, and helper calls that index arrays with it read the elements without the double index checks.
This commit is contained in:
@@ -867,6 +867,80 @@ test("an array length bounds while and for counters even when the body resizes t
|
||||
expect(analyzeIntegerRanges(g).get(cursor)).toEqual({ min: 0, max: 4294967295 });
|
||||
});
|
||||
|
||||
test("a counter plus or minus a literal compared with a bound narrows the counter", () => {
|
||||
// for (let i = 0; i + 1 < items.length; i++) { pair(items, i, i + 1); }
|
||||
const ARR = { kind: "array", elem: F64 } as const;
|
||||
const items: IrExpr = { loc, kind: "varRef", localId: "items", type: ARR };
|
||||
const length: IrExpr = {
|
||||
loc,
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: items,
|
||||
args: [],
|
||||
type: F64,
|
||||
};
|
||||
const inside = ref("i"),
|
||||
next = bin("+", ref("i"), num(1)),
|
||||
step = bin("+", ref("i"), num(1));
|
||||
const call: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee: "pair", args: [items, inside, next], type: F64 },
|
||||
};
|
||||
const f = fn([
|
||||
{
|
||||
loc,
|
||||
kind: "for",
|
||||
init: { loc, kind: "varDecl", localId: "i", init: num(0) },
|
||||
cond: condition("<", bin("+", ref("i"), num(1)), length),
|
||||
update: { loc, kind: "assign", localId: "i", value: step },
|
||||
body: [call],
|
||||
},
|
||||
]);
|
||||
f.locals.push(
|
||||
{ id: "i", name: "i", type: F64, mutable: true },
|
||||
{ id: "items", name: "items", type: ARR, mutable: false },
|
||||
);
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
expect(ranges.get(inside)).toEqual({ min: 0, max: 4294967293 });
|
||||
expect(ranges.get(next)).toEqual({ min: 1, max: 4294967294 });
|
||||
expect(ranges.get(step)).toEqual({ min: 1, max: 4294967294 });
|
||||
|
||||
// if (1 + x < 10) observe(x); if (x - 2 >= 0) observe(x); with x in [0, 100]
|
||||
const below = ref(),
|
||||
above = ref(),
|
||||
wide = ref();
|
||||
const g = fn([
|
||||
assign(bin("&", ref("unknown"), num(100))),
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition("<", bin("+", num(1), ref()), num(10)),
|
||||
then: [observe(below)],
|
||||
else_: null,
|
||||
},
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition(">=", bin("-", ref(), num(2)), num(0)),
|
||||
then: [observe(above)],
|
||||
else_: null,
|
||||
},
|
||||
// A bound beyond 2^52 narrows nothing through an offset.
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition("<", bin("+", ref(), num(1)), num(2 ** 53)),
|
||||
then: [observe(wide)],
|
||||
else_: null,
|
||||
},
|
||||
]);
|
||||
const gRanges = analyzeIntegerRanges(g);
|
||||
expect(gRanges.get(below)).toEqual({ min: 0, max: 8 });
|
||||
expect(gRanges.get(above)).toEqual({ min: 2, max: 100 });
|
||||
expect(gRanges.get(wide)).toEqual({ min: 0, max: 100 });
|
||||
});
|
||||
|
||||
test("whole-program slot facts seed parameters, field reads and call results", () => {
|
||||
const field: IrExpr = {
|
||||
loc,
|
||||
|
||||
@@ -215,6 +215,24 @@ export interface IntegerSlotFacts {
|
||||
elements?: (arr: IrExpr) => boolean;
|
||||
}
|
||||
|
||||
/** A local read, or a numeric local plus or minus an integer literal
|
||||
* (`i + 1`, `1 + i`, `i - 2`): the local and its offset. */
|
||||
function offsetRef(e: IrExpr): { localId: string; offset: number } | null {
|
||||
if (e.kind === "varRef") return { localId: e.localId, offset: 0 };
|
||||
if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-") || e.type.kind !== "f64") return null;
|
||||
const literal = (x: IrExpr): number | null =>
|
||||
x.kind === "numLit" && Number.isSafeInteger(x.value) && !Object.is(x.value, -0)
|
||||
? x.value
|
||||
: null;
|
||||
const right = literal(e.right);
|
||||
if (e.left.kind === "varRef" && e.left.type.kind === "f64" && right !== null)
|
||||
return { localId: e.left.localId, offset: e.op === "+" ? right : -right };
|
||||
const left = literal(e.left);
|
||||
if (e.op === "+" && e.right.kind === "varRef" && e.right.type.kind === "f64" && left !== null)
|
||||
return { localId: e.right.localId, offset: left };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Structured local dataflow. Calls cannot write uncaptured local slots,
|
||||
* but argument expressions can; unknown evaluation forms invalidate all
|
||||
* syntactically written slots before their children are inspected. Loops
|
||||
@@ -508,7 +526,7 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
let ref = condition.left;
|
||||
let bound = condition.right;
|
||||
let op: string = condition.op;
|
||||
if (ref.kind !== "varRef" && bound.kind === "varRef") {
|
||||
if (offsetRef(ref) === null && offsetRef(bound) !== null) {
|
||||
[ref, bound] = [bound, ref];
|
||||
op =
|
||||
(
|
||||
@@ -518,18 +536,31 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
>
|
||||
)[op] ?? "";
|
||||
}
|
||||
if (ref.kind !== "varRef") return;
|
||||
const boundRange =
|
||||
const target = offsetRef(ref);
|
||||
if (target === null) return;
|
||||
const literalBound =
|
||||
bound.kind === "numLit" && Number.isFinite(bound.value)
|
||||
? { min: bound.value, max: bound.value }
|
||||
: ranges.get(bound);
|
||||
if (!boundRange) return;
|
||||
if (!literalBound) return;
|
||||
// An Int32 contract local holds an int32 even where the dataflow has
|
||||
// no fact for it (loop headers, after unknown regions).
|
||||
const previous =
|
||||
facts.get(ref.localId) ??
|
||||
(eligible.has(ref.localId) ? (slots.locals?.(ref.localId) ?? undefined) : undefined);
|
||||
facts.get(target.localId) ??
|
||||
(eligible.has(target.localId) ? (slots.locals?.(target.localId) ?? undefined) : undefined);
|
||||
if (!previous) return;
|
||||
// `x + c < b` bounds x as `x < b - c` does. A sum that rounds has a
|
||||
// magnitude of at least 2^53, so against a bound within 2^52 every
|
||||
// comparison answers as the exact sum would.
|
||||
const offset = target.offset;
|
||||
if (
|
||||
offset !== 0 &&
|
||||
(Math.abs(offset) > 2 ** 31 ||
|
||||
Math.abs(literalBound.min) > 2 ** 52 ||
|
||||
Math.abs(literalBound.max) > 2 ** 52)
|
||||
)
|
||||
return;
|
||||
const boundRange = { min: literalBound.min - offset, max: literalBound.max - offset };
|
||||
if (!truth)
|
||||
op =
|
||||
(
|
||||
@@ -552,7 +583,7 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
else if (max === boundRange.max) max -= 1;
|
||||
}
|
||||
if (min <= max && Number.isInteger(min) && Number.isInteger(max))
|
||||
facts.set(ref.localId, { min, max });
|
||||
facts.set(target.localId, { min, max });
|
||||
}
|
||||
function expr(e: IrExpr, facts: Facts): IntegerRange | null {
|
||||
let range: IntegerRange | null = null;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5685-loop-counter-offset-bounds.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Loops whose condition compares the counter plus or minus a literal with a
|
||||
// bound (`i + 1 < xs.length`, `1 + i < n`, `i - 1 >= 0`): fractional, NaN,
|
||||
// infinite and negative bounds, counters past 2^31 and near 2^53, helper
|
||||
// calls with the counter, string lengths and arrays that grow in the body.
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
function pointAt(xs: number[], ys: number[], i: number): Point {
|
||||
return { x: xs[i], y: ys[i] };
|
||||
}
|
||||
function gap(p: Point, q: Point): number {
|
||||
return Math.abs(p.x - q.x) + Math.abs(p.y - q.y);
|
||||
}
|
||||
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
xs.push((i * 37) % 11);
|
||||
ys.push((i * 53) % 7);
|
||||
}
|
||||
|
||||
let path = 0;
|
||||
for (let i = 0; i + 1 < xs.length; i++) path += gap(pointAt(xs, ys, i), pointAt(xs, ys, i + 1));
|
||||
console.log("path", path);
|
||||
|
||||
let pairs = 0;
|
||||
for (let i = 0; 1 + i < xs.length; i++) pairs += xs[i]! * xs[i + 1]!;
|
||||
console.log("pairs", pairs);
|
||||
|
||||
let triples = 0;
|
||||
for (let i = 0; i + 2 <= xs.length - 1; i += 2) triples += xs[i]! + xs[i + 1]! + xs[i + 2]!;
|
||||
console.log("triples", triples);
|
||||
|
||||
// Bounds that are not integers, or not finite.
|
||||
function count(bound: number): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i + 2 < bound; i++) {
|
||||
n++;
|
||||
if (n > 20) break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
console.log("bounds", count(7.5), count(2), count(2.0001), count(-3), count(Number.NaN), count(Infinity));
|
||||
|
||||
// Counters below zero, past int32 and near the end of exact integers.
|
||||
let negative = 0;
|
||||
for (let i = -5; i + 5 < 3; i++) negative += i;
|
||||
console.log("negative", negative);
|
||||
let wide = 0;
|
||||
for (let i = 2 ** 31 - 3; i + 1 < 2 ** 31 + 3; i++) wide += i - 2 ** 31;
|
||||
console.log("past int32", wide);
|
||||
const exact: number[] = [];
|
||||
for (let i = 2 ** 53 - 4; i + 1 < 2 ** 53; i++) exact.push(i - 2 ** 53);
|
||||
console.log("near 2^53", exact.join(","));
|
||||
let half = 0;
|
||||
for (let i = 0; i + 0.5 < 4; i++) half += i;
|
||||
console.log("half offset", half);
|
||||
|
||||
// Descending counters.
|
||||
const down: number[] = [];
|
||||
for (let i = 6; i - 1 >= 0; i--) down.push(i);
|
||||
console.log("down", down.join(","));
|
||||
let back = 0;
|
||||
for (let i = xs.length - 1; i - 1 >= 0; i--) back += xs[i]! - xs[i - 1]!;
|
||||
console.log("back", back);
|
||||
|
||||
// A while loop on a string's length.
|
||||
const text = "abcdefghij";
|
||||
let pos = 0;
|
||||
let codes = 0;
|
||||
while (pos + 3 < text.length) {
|
||||
codes += text.charCodeAt(pos) * text.charCodeAt(pos + 3);
|
||||
pos++;
|
||||
}
|
||||
console.log("string", pos, codes);
|
||||
|
||||
// An array that grows inside the loop: the bound is reread on every pass.
|
||||
const grow = [1, 2, 3];
|
||||
let steps = 0;
|
||||
for (let i = 0; i + 1 < grow.length; i++) {
|
||||
if (grow.length < 12) grow.push(grow[i]! + grow[i + 1]!);
|
||||
steps++;
|
||||
}
|
||||
console.log("grow", steps, grow.join(","));
|
||||
|
||||
// The counter after the loop, and an equality bound.
|
||||
let last = 0;
|
||||
for (last = 0; last + 1 < 9; last++) {}
|
||||
console.log("last", last);
|
||||
let hits = 0;
|
||||
for (let i = 0; i < 10; i++) if (i + 3 === 7) hits += i;
|
||||
console.log("equal", hits);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2450, "san": 2700 }
|
||||
Reference in New Issue
Block a user