Files
scriptc/packages/compiler/test/regex-replacers.test.ts
T
Chris Tate dec6be6ebd Run global substitutions with a literal replacement through build-time replacers
A global replace or replaceAll of a regex literal called the compiled
matcher once per match: each call set up its frame and seek again, and the
runtime then copied the gap and the replacement. A substitution of a
literal inside the compiled subset with an ASCII replacement without $
patterns now gets a replacer built for that pattern and replacement. It
appends each gap and the replacement to the output and continues the
search after the match, as the runtime's loop does (one past an empty
match), without returning. Gaps of up to 16 bytes are one 16-byte copy,
and the search after a match is a 16-byte scan instead of a memchr call.

scr_regex_replace_with runs the replacer on an ASCII subject; a subject
outside ASCII keeps the runtime's loop. Plain-text patterns, which the
runtime searches as strings, get no replacer.

New corpus program: 8351-regex-literal-replacers.ts.
2026-10-25 13:35:04 -05:00

43 lines
1.6 KiB
TypeScript

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 } from "../src/index.js";
async function emitLlvm(source: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-regex-replacers-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ll");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
return await readFile(outPath, "utf8");
} finally {
await rm(dir, { recursive: true, force: true });
}
}
test("global substitutions of regex literals with literal replacements run a replacer", async () => {
const llvm = await emitLlvm(`
const s = process.argv[2] ?? "a<b>c";
const rep = process.argv[3] ?? "-";
console.log(
s.replace(/<[^>]*>/g, "|"),
s.replaceAll(/a[bc]/g, "<2>"),
s.replace(/<[^>]*>/g, "|"),
s.replace(/<[^>]*>/g, "$&$&"),
s.replace(/<[^>]*>/, "|"),
s.replace(/<[^>]*>/gy, "|"),
s.replace(/<[^>]*>/g, rep),
);
`);
const replacers = llvm.match(/^define internal i32 @sc_rr_\d+\(/gm) ?? [];
// One replacer per (pattern, flags, replacement): the repeated site shares it.
expect(replacers).toHaveLength(2);
expect(llvm.match(/call ptr @scr_regex_replace_with\(/g)).toHaveLength(3);
// $ patterns, non-global and sticky patterns, and computed replacements
// keep the runtime's loop.
expect(llvm.match(/call ptr @scr_regex_replace\(/g)!.length).toBeGreaterThanOrEqual(4);
});