mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Let scan reads reload after string read fallbacks
This commit is contained in:
@@ -21,15 +21,13 @@ import { exactIndexLines } from "./common.js";
|
||||
|
||||
export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)";
|
||||
export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)";
|
||||
/** The runtime fallbacks are cold: both common cases read inline, so a
|
||||
* loop keeps its values in registers around the calls. They read only the
|
||||
* receiver and write only runtime-private caches (index entries, unit
|
||||
* arrays) and the proven-ASCII bit, a hint that is safe to read stale. A
|
||||
* string's content changes only through runtime calls that write memory
|
||||
* (in-place append), which these attributes leave ordered. */
|
||||
const LENGTH_CALL_ATTRS = "cold nocallback nounwind willreturn memory(argmem: read)";
|
||||
const READ_CALL_ATTRS =
|
||||
"cold nocallback nounwind willreturn memory(argmem: read, inaccessiblemem: readwrite)";
|
||||
/** The runtime fallbacks are cold: both common cases read inline. They
|
||||
* carry no
|
||||
* memory restriction: a fallback can evict an index entry, which resets
|
||||
* the published scan receiver (@scr_str_scan, read inline below) and frees
|
||||
* its unit array, so every scan read after the call must reload. */
|
||||
const LENGTH_CALL_ATTRS = "cold nocallback nounwind willreturn";
|
||||
const READ_CALL_ATTRS = "cold nocallback nounwind willreturn";
|
||||
/** Requests the integer-index helper below; an LLVM comment line among the
|
||||
* declarations, so it never declares anything itself. */
|
||||
export const STRING_CHAR_CODE_AT_INT_MARKER = "; uses @sc_str_char_code_at_int";
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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, validateModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
|
||||
// The runtime fallbacks of the inline string reads can evict an index
|
||||
// entry: that resets the published scan receiver (@scr_str_scan) and frees
|
||||
// its unit array. A memory restriction on those calls would let LLVM keep
|
||||
// the receiver's units in a register across them and read freed memory.
|
||||
test("string read fallbacks let scan receiver reads reload after every call", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-string-scan-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
function scan(s: string, a: string, b: string): number {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < s.length; i++) sum += s.charCodeAt(i) + a.length + b.length;
|
||||
return sum;
|
||||
}
|
||||
function at(s: string, i: number): number {
|
||||
return s.charCodeAt(i);
|
||||
}
|
||||
console.log(scan("é-é", "ü-ü", "ö-ö"), at("é", 0.5));
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
const llvm = emitLlvmModule(module, { pointerBits: 64 });
|
||||
expect(llvm).toContain("@scr_str_scan = external global");
|
||||
const fallbacks = llvm
|
||||
.split("\n")
|
||||
.filter((line) => /call double @scr_str_(utf16_len|char_code_at)\(/.test(line));
|
||||
expect(fallbacks.length).toBeGreaterThanOrEqual(3);
|
||||
for (const line of fallbacks) expect(line).not.toMatch(/\bmemory\(/);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5170-string-scan-eviction.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Indexed reads of a non-ASCII string stay correct while length reads of
|
||||
// other non-ASCII strings evict the runtime's per-string unit caches.
|
||||
function build(seed: number, n: number): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < n; i++) s += i % 7 === 0 ? "é" : String.fromCharCode(97 + ((seed + i) % 26));
|
||||
return s;
|
||||
}
|
||||
|
||||
// Reading every unit publishes `s` as the runtime's scan receiver.
|
||||
function warm(s: string): number {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < s.length; i++) sum += s.charCodeAt(i);
|
||||
return sum;
|
||||
}
|
||||
|
||||
// Four receivers longer than 64 bytes: reading their lengths claims every
|
||||
// index slot between two reads of `s`.
|
||||
function scan(s: string, a: string, b: string, c: string, d: string): number {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
sum = (sum * 31 + s.charCodeAt(i)) % 1000003;
|
||||
if (i % 37 === 36) sum = (sum + a.length + b.length + c.length + d.length) % 1000003;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
function scanAt(s: string, a: string, b: string, c: string, d: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
out += s.charAt(i);
|
||||
if (i % 41 === 40) out += a.length + b.length + c.length + d.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const others = [build(1, 70), build(2, 71), build(4, 72), build(6, 73)];
|
||||
for (const n of [600, 4000]) {
|
||||
const subject = build(n, n);
|
||||
console.log(warm(subject));
|
||||
console.log(scan(subject, others[0]!, others[1]!, others[2]!, others[3]!));
|
||||
console.log(warm(subject));
|
||||
console.log(scan(subject, others[0]!, others[1]!, others[2]!, others[3]!));
|
||||
}
|
||||
const tail = build(5, 200);
|
||||
console.log(warm(tail));
|
||||
console.log(scanAt(tail, others[0]!, others[1]!, others[2]!, others[3]!));
|
||||
Reference in New Issue
Block a user