mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
`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.
95 lines
4.1 KiB
TypeScript
95 lines
4.1 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
import { compile, deserializeModule } from "../src/index.js";
|
|
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
|
|
|
test("scans of strings that are not ASCII read their UTF-16 units", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-loops-"));
|
|
try {
|
|
const entry = join(dir, "main.ts");
|
|
await writeFile(
|
|
entry,
|
|
`
|
|
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 skip(s: string): number {
|
|
let pos = 0;
|
|
while (pos < s.length && s.charCodeAt(pos) !== 32) pos++;
|
|
return pos;
|
|
}
|
|
function pairs(s: string, t: string): number {
|
|
let n = 0;
|
|
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === t.charCodeAt(i % t.length)) n++;
|
|
return n;
|
|
}
|
|
let text = "";
|
|
for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade ";
|
|
const other = text.replace(/a/g, "ä");
|
|
let total = 0;
|
|
for (let pass = 0; pass < 4; pass++)
|
|
total += spaces(text) + spaces(other) + pairs(text, other) + skip(text);
|
|
console.log(total, spaces("plain ascii text"), spaces(""));
|
|
`,
|
|
);
|
|
const outPath = join(dir, "main.ir.json");
|
|
const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
|
if (!lowered.ok)
|
|
throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
|
|
const body = (name: string): string =>
|
|
ll.match(
|
|
new RegExp(
|
|
`define internal [^\\n]+ @sc_bf_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`,
|
|
),
|
|
)![0];
|
|
|
|
const spaces = body("spaces");
|
|
// A string that is not ASCII is published as the scan receiver once,
|
|
// before the loop, and its copy loads units.
|
|
expect(spaces).toMatch(/call double @scr_str_char_code_at\(ptr %\S+, double 0\.0\)/);
|
|
// The copies follow the checked one: bytes, then units.
|
|
const bytesAt = spaces.search(/^bytes\.version\.fast\d+:/m);
|
|
const unitsAt = spaces.search(/^bytes\.version\.fast\.units\d+:/m);
|
|
expect(bytesAt).toBeGreaterThan(0);
|
|
expect(unitsAt).toBeGreaterThan(bytesAt);
|
|
const units = spaces.slice(unitsAt);
|
|
expect(units).toMatch(/load i16, ptr %\S+\n\s+%\S+ = uitofp i16/);
|
|
expect(units).not.toContain("@sc_str_char_code_at_int");
|
|
// An ASCII string still reads bytes.
|
|
const bytes = spaces.slice(bytesAt, unitsAt);
|
|
expect(bytes).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
|
|
expect(bytes).not.toContain("@sc_str_char_code_at_int");
|
|
// A while loop over a counter below the length reads units too.
|
|
const skip = body("skip");
|
|
expect(skip).toMatch(/^bytes\.version\.fast\.units\d+:/m);
|
|
// Its fast copies read the length the guard took; the checked copy
|
|
// still asks for it.
|
|
const skipFastAt = skip.search(/^bytes\.version\.fast\d+:/m);
|
|
expect(skip.slice(0, skipFastAt)).toContain("@sc_str_utf16_len");
|
|
expect(skip.slice(skipFastAt)).not.toContain("@sc_str_utf16_len");
|
|
// Two strings cannot both be the scan receiver.
|
|
expect(body("pairs")).not.toContain("bytes.version.fast.units");
|
|
|
|
const exe = join(dir, "main");
|
|
const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe });
|
|
if (!built.ok) throw new Error(JSON.stringify(built.diagnostics));
|
|
let text = "";
|
|
for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade ";
|
|
const other = text.replace(/a/g, "ä");
|
|
const count = (s: string): number => s.split(" ").length - 1;
|
|
let pairs = 0;
|
|
for (let i = 0; i < text.length; i++)
|
|
if (text.charCodeAt(i) === other.charCodeAt(i % other.length)) pairs++;
|
|
const total = 4 * (count(text) + count(other) + pairs + text.indexOf(" "));
|
|
expect(execFileSync(exe, { encoding: "utf8" })).toBe(`${total} 2 0\n`);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|