Files
scriptc/tests/corpus/7305-string-unit-loop-versions.ts
T
Chris Tate 5d8804db38 Version string scans over the UTF-16 units of strings that are not ASCII
`for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++`
over a text with any non-ASCII character failed the loop version's ASCII
guard and ran the checked loop. Each read tested the ASCII bit, compared
the string with the runtime's published scan receiver, tested the
bounds, loaded the units pointer, and kept the opaque fallback.

The guard now also accepts a version's one string receiver when it is
not ASCII. One runtime read publishes the string as the scan receiver
(ScrStrScan), whose UTF-16 units the runtime holds once a scan has
read it enough. The guard then tests the accesses against the published
unit count, and a third copy of the loop reads each unit with one
16-bit load from the units pointer taken in the guard.

The guard takes that path only while the runtime publishes the receiver
itself, so the length it took is the receiver's UTF-16 length on both
fast paths. Both fast copies read it for the receiver's `length`, which
a while loop's condition otherwise asks the runtime for on every
iteration.

The runtime drops a string's units when the string is released or
appended to in place, or when its index entry is evicted because
another non-ASCII string is indexed. The receiver is a binding the loop
does not write, so the first two cannot happen while the loop runs. A
units copy is emitted only for a loop that uses unit-safe operations
(string-unit-versions.ts): numbers, locals, records and class fields,
typed arrays, number arrays, Math, the receiver's charCodeAt and length,
and calls of functions made of the same operations that read no string.

Tests of `s.charAt(i)` against one-unit literals now become charCodeAt
compares in the IR pass (char-unit-locals.ts), so a version sees one
kind of read.
2026-10-23 22:40:04 -05:00

220 lines
6.3 KiB
TypeScript

// charCodeAt scans of strings that are not ASCII, repeated so the runtime
// holds their UTF-16 units: counts, hashes, neighbor reads, descending
// loops, reads past the end, line starts pushed to a number array, helper
// functions that read no string, a helper that reads another string, a
// second non-ASCII string read in the same loop, two texts scanned in
// turn, a short text, a text appended to between scans, surrogate pairs
// and lone surrogates, ASCII texts beside them, and while loops over a
// counter below the length: word and identifier scans, peeks past the
// counter, and a lexer class whose position is a field. Loops also read
// the length in their bodies, and loops over a fresh text that no scan
// has published run from its end and over its last units.
function isWordUnit(c: number): boolean {
return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c > 127;
}
function isWordOrDigit(c: number): boolean {
return isWordUnit(c) || (c >= 48 && c <= 57);
}
function spaces(s: string): number {
let n = 0;
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++;
return n;
}
function hash(s: string): number {
let h = 7;
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
return h;
}
function rises(s: string): number {
let n = 0;
for (let i = 1; i < s.length - 1; i++)
if (s.charCodeAt(i) > s.charCodeAt(i - 1) && s.charCodeAt(i) >= s.charCodeAt(i + 1)) n++;
return n;
}
function backwards(s: string): number {
let h = 0;
for (let i = s.length - 1; i >= 0; i -= 3) h = (h * 7 + s.charCodeAt(i)) % 1000003;
return h;
}
function pastEnd(s: string): string {
let sum = 0;
for (let i = 0; i <= s.length; i++) sum += s.charCodeAt(i);
return String(sum);
}
function lineStarts(s: string): number[] {
const starts = [0];
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) starts.push(i + 1);
return starts;
}
function wordUnits(s: string): number {
let n = 0;
for (let i = 0; i < s.length; i++) if (isWordOrDigit(s.charCodeAt(i))) n++;
return n;
}
function codeOf(t: string, i: number): number {
return t.charCodeAt(i % t.length);
}
function mixed(s: string, t: string): number {
let n = 0;
for (let i = 0; i < s.length; i++) n = (n + s.charCodeAt(i) * codeOf(t, i)) % 1000003;
return n;
}
function sameAt(a: string, b: string): number {
let n = 0;
const end = Math.min(a.length, b.length);
for (let i = 0; i < end; i++) if (a.charCodeAt(i) === b.charCodeAt(i)) n++;
return n;
}
function surrogates(s: string): string {
let high = 0,
low = 0;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c >= 0xd800 && c < 0xdc00) high++;
else if (c >= 0xdc00 && c < 0xe000) low++;
}
return `${high}/${low}`;
}
function wordCount(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 wordUnitRuns(s: string): number {
let pos = 0,
total = 0;
while (pos < s.length) {
if (isWordUnit(s.charCodeAt(pos))) {
const start = pos;
while (pos < s.length && isWordOrDigit(s.charCodeAt(pos))) pos++;
total += pos - start;
} else pos = (pos + 1) | 0;
}
return total;
}
function doubled(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;
}
class Scanner {
pos = 0;
src: string;
constructor(src: string) {
this.src = src;
}
line(): number {
const src = this.src;
const start = this.pos;
let pos = start;
while (pos < src.length && src.charCodeAt(pos) !== 10) pos++;
this.pos = pos + 1;
return pos - start;
}
}
function lineLengths(s: string): number {
const scanner = new Scanner(s);
let total = 0,
lines = 0;
while (scanner.pos < s.length) {
total = (total * 31 + scanner.line()) % 1000003;
lines++;
}
return total * 1000 + lines;
}
function remaining(s: string): number {
let pos = 0;
let sum = 0;
while (pos < s.length) {
if (s.charCodeAt(pos) === 32) sum += s.length - pos;
pos++;
}
return sum;
}
function lastLineLength(s: string): number {
let last = s.length;
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) last = s.length - i - 1;
return last;
}
function from(s: string, start: number): number {
let pos = start;
let sum = 0;
while (pos < s.length) {
sum = sum * 3 + s.charCodeAt(pos);
pos++;
}
return sum;
}
const words = ["Größe", "naïve", "façade", "東京", "ok", "✓", "mañana", "plain", "😀", "end\n"];
function text(count: number, seed: number): string {
let out = "";
for (let k = 0; k < count; k++) out += words[(k * 7 + seed) % words.length] + (k % 5 === 0 ? " " : " ");
return out;
}
const big = text(3000, 1);
const other = text(2000, 4);
const ascii = big.replace(/[^\x00-\x7f]/g, "x");
const short = "café ✓ 東京";
const lone = "a\uD800b\uDFFFc " + big.slice(0, 500) + "\uD83D";
for (let pass = 0; pass < 3; pass++) {
console.log(
pass,
spaces(big),
hash(big),
rises(big),
backwards(big),
pastEnd(big),
lineStarts(big).length,
wordUnits(big),
);
console.log(" ", spaces(other), hash(other), spaces(big), hash(other), wordUnits(other));
console.log(" ", mixed(big, other), sameAt(big, other), surrogates(big), surrogates(lone));
console.log(" ", spaces(ascii), hash(ascii), spaces(short), hash(short), hash(lone), rises(lone));
console.log(" ", wordCount(big), wordUnitRuns(big), doubled(big), lineLengths(big));
console.log(" ", wordCount(ascii), wordUnitRuns(other), doubled(lone), lineLengths(lone), wordCount(""));
const fresh = text(40 + pass, pass) + "ü";
console.log(" ", remaining(big), remaining(ascii), lastLineLength(big), lastLineLength(other));
console.log(" ", from(fresh, fresh.length), from(fresh, fresh.length - 3), from(big, big.length - 4));
}
let grown = text(1500, 2);
for (let round = 0; round < 3; round++) {
console.log("grown", round, grown.length, spaces(grown), hash(grown), hash(grown));
grown += text(200, round) + "Ünïcödé";
}
console.log("empty", spaces(""), hash(""), pastEnd(""), lineStarts("").length);