mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Bound string reads of while loops by their counter tests
With while-loop versions, `while (pos < end && s.charCodeAt(pos) === 32) pos++` took a version whose guard could never hold. The counter scan records only typed-array accesses, so the string read fell back to the statically ranged check. For a counter held in an int32 slot, that check asks for a length above 2^31 - 2. The loop paid for the guard and ran its checked copy. The counter scan now records `s.charCodeAt(i)` and `s.charAt(i)` of a string like typed-array reads. Their guard compares the counter's bound with the string's length, as for a typed array. Corpus 7302 adds while loops over counters below a string's length.
This commit is contained in:
@@ -557,3 +557,48 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () =>
|
||||
// The checked copy keeps the inline helper and its fallback.
|
||||
expect(body).toContain("@sc_str_char_code_at_int");
|
||||
});
|
||||
|
||||
test("while loop counters below a string's length version its reads", () => {
|
||||
// while (i < text.length && text.charCodeAt(i) === 32) i = i + 1
|
||||
const text = ref("text", STR);
|
||||
const read = charCode(text, ref("i"));
|
||||
const length: IrExpr = {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: text,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const loop: IrStmt & { kind: "while" } = {
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "bin", op: "<", left: ref("i"), right: length, type: BOOL, loc },
|
||||
right: { kind: "bin", op: "===", left: read, right: num(32), type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [{ kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }],
|
||||
loc,
|
||||
};
|
||||
const f = textKernel([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
|
||||
const version = matchBytesLoopVersion(loop, null, {
|
||||
locals: new Map(f.locals.map((l) => [l.id, l])),
|
||||
invariantGlobals: new Set(),
|
||||
captured: new Set(),
|
||||
ranges: analyzeIntegerRanges(f),
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
scalarStrings: new Set(),
|
||||
workers: false,
|
||||
owned: new Set(),
|
||||
})!;
|
||||
// The read is bounded by the counter's test against the length, not by
|
||||
// a static range the guard could not meet.
|
||||
expect(version.bounds.has(read)).toBe(true);
|
||||
expect(version.receivers.map((r) => r.string)).toEqual([true]);
|
||||
expect(version.bounded.map((c) => c.slack)).toEqual([0]);
|
||||
expect(version.fixed).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -711,17 +711,18 @@ function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | nul
|
||||
return { id: s.localId, k: value.right.value, wraps };
|
||||
}
|
||||
|
||||
/** Typed-array accesses of a while loop that happen at a counter below a
|
||||
* loop-invariant bound. A counter is a number local that the loop changes
|
||||
* only by positive integer increments, written as statements of the body,
|
||||
* of its blocks and branches, or of nested while loops. The condition of
|
||||
* the loop, an `if` or a `&&` chain that compares the counter below a bound
|
||||
* (`while (pos < end && bytes[pos] === 32) pos++`) makes it below that
|
||||
* bound until the next increment on the path. The counter starts at its
|
||||
* entry value and only grows (a `| 0` increment never wraps when every
|
||||
* bound is at most 2^31 minus the increments), so an access at `counter +
|
||||
* d` is in bounds for the whole loop when the entry value is at least `-d`
|
||||
* and `bound + d < length` (`+ 1` for `<=`). */
|
||||
/** Typed-array and string accesses of a while loop that happen at a
|
||||
* counter below a loop-invariant bound. A counter is a number local that
|
||||
* the loop changes only by positive integer increments, written as
|
||||
* statements of the body, of its blocks and branches, or of nested while
|
||||
* loops. The condition of the loop, an `if` or a `&&` chain that compares
|
||||
* the counter below a bound (`while (pos < end && bytes[pos] === 32)
|
||||
* pos++`, or `s.charCodeAt(pos)` of a string) makes it below that bound
|
||||
* until the next increment on the path. The counter starts at its entry
|
||||
* value and only grows (a `| 0` increment never wraps when every bound is
|
||||
* at most 2^31 minus the increments), so an access at `counter + d` is in
|
||||
* bounds for the whole loop when the entry value is at least `-d` and
|
||||
* `bound + d < length` (`+ 1` for `<=`). */
|
||||
function findWhileCounterAccesses(
|
||||
loop: IrStmt & { kind: "while" },
|
||||
host: {
|
||||
@@ -831,6 +832,12 @@ function findWhileCounterAccesses(
|
||||
: undefined;
|
||||
if (index) record(e, index, clean);
|
||||
}
|
||||
if (
|
||||
e.kind === "strIntrinsic" &&
|
||||
(e.method === "charCodeAt" || e.method === "charAt") &&
|
||||
e.args[0]
|
||||
)
|
||||
record(e, e.args[0], clean);
|
||||
if (e.kind === "logical" && e.op === "&&") {
|
||||
expr(e.left, clean);
|
||||
expr(e.right, withFacts(clean, e.left));
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// text, surrogate pairs and lone surrogates, empty strings, neighbors at
|
||||
// i - 1 and i + 1, loops that stop early or read past the end, strings built
|
||||
// by concatenation and slicing, a scan of a slice local, two strings read at
|
||||
// one index, a string rebound inside or after the loop, nested scans and a
|
||||
// module-level scan.
|
||||
// one index, a string rebound inside or after the loop, nested scans, a
|
||||
// module-level scan, and while loops over a counter below the length: word
|
||||
// and identifier scans, peeks past the counter, a counter from a field, and
|
||||
// reads at or past the end.
|
||||
|
||||
function spaces(s: string): number {
|
||||
let n = 0;
|
||||
@@ -75,6 +77,75 @@ function rebound(s: string): number {
|
||||
return n;
|
||||
}
|
||||
|
||||
function words(s: string): number {
|
||||
let pos = 0,
|
||||
n = 0;
|
||||
const end = s.length;
|
||||
while (pos < end) {
|
||||
while (pos < end && s.charCodeAt(pos) === 32) pos++;
|
||||
if (pos >= end) break;
|
||||
n++;
|
||||
while (pos < end && s.charCodeAt(pos) !== 32) pos++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function identifiers(s: string): number {
|
||||
let pos = 0,
|
||||
total = 0;
|
||||
while (pos < s.length) {
|
||||
const c = s.charCodeAt(pos);
|
||||
if (c >= 97 && c <= 122) {
|
||||
const start = pos;
|
||||
while (pos < s.length && s.charCodeAt(pos) >= 97 && s.charCodeAt(pos) <= 122) pos++;
|
||||
total += pos - start;
|
||||
} else pos = (pos + 1) | 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function pairs(s: string): number {
|
||||
let pos = 0,
|
||||
n = 0;
|
||||
while (pos < s.length) {
|
||||
if (s.charCodeAt(pos) === s.charCodeAt(pos + 1)) n++;
|
||||
pos += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function throughEnd(s: string): string {
|
||||
let pos = 0,
|
||||
sum = 0;
|
||||
while (pos <= s.length) {
|
||||
sum += s.charCodeAt(pos);
|
||||
pos++;
|
||||
}
|
||||
return String(sum);
|
||||
}
|
||||
|
||||
class Cursor {
|
||||
pos = 0;
|
||||
src: string;
|
||||
constructor(src: string) {
|
||||
this.src = src;
|
||||
}
|
||||
skip(): number {
|
||||
const src = this.src;
|
||||
let pos = this.pos;
|
||||
while (pos < src.length && src.charCodeAt(pos) !== 44) pos++;
|
||||
this.pos = pos + 1;
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
function fields(s: string): string {
|
||||
const cursor = new Cursor(s);
|
||||
const ends: number[] = [];
|
||||
while (cursor.pos < s.length) ends.push(cursor.skip());
|
||||
return ends.join(",");
|
||||
}
|
||||
|
||||
const samples = [
|
||||
"the quick brown fox jumps over the lazy dog",
|
||||
"",
|
||||
@@ -99,6 +170,7 @@ for (const s of samples) {
|
||||
rebound(s),
|
||||
slicedScan(s, 2, 9),
|
||||
);
|
||||
console.log(" ", words(s), identifiers(s), pairs(s), throughEnd(s), fields(s));
|
||||
}
|
||||
|
||||
let built = "";
|
||||
|
||||
Reference in New Issue
Block a user