Run global substitutions with a literal replacement through build-time replacers (#2240)

This commit is contained in:
Chris Tate
2026-10-25 14:19:37 -05:00
committed by GitHub
12 changed files with 491 additions and 23 deletions
@@ -600,6 +600,23 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
regexMatchers.set(key, code);
for (const decl of code.decls) emitter.declare(decl);
}
// Replacers of global substitutions with a literal replacement; the call
// sites register only patterns inside the compiled subset.
const regexReplacers: RegexMatcherCode[] = [];
for (const rr of emitter.regexReplacers.values()) {
const code = compileRegexMatcher(
rr.sym,
rr.pattern,
rr.flags,
emitter.sizeType,
!emitter.wasi,
rr.replacement,
);
if (code === null)
throw new InternalCompilerError(`llvm emitter bug: replacer ${rr.sym} did not compile`);
regexReplacers.push(code);
for (const decl of code.decls) emitter.declare(decl);
}
const out: string[] = [
`; Generated by scriptc (LLVM backend) from ${emitter.mod.sourceFile}. Do not edit.`,
@@ -882,6 +899,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
}
if (emitter.regexInstances.size > 0) out.push(``);
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
for (const code of regexReplacers) out.push(...code.lines, ``);
for (const [, inst] of emitter.templateStringsInstances) {
// One immortal ScrArr per tagged-template site: a data global of
// interned cooked-string literals, and the ScrArr header over it
@@ -1303,6 +1303,12 @@ export class LlEmitter {
string,
{ sym: string; src: string; fl: string; pattern: string; flags: string }
>();
/** Build-time replacers: one per (pattern, flags, replacement) of a
* global substitution with a literal replacement (regex-matchers.ts). */
readonly regexReplacers = new Map<
string,
{ sym: string; pattern: string; flags: string; replacement: string }
>();
/** Interned tagged-template strings objects: per-site key → { symbol,
* interned cooked-literal refs }. One immortal ScrArr of string slots
* per template SITE (the spec's per-occurrence identity — the C
@@ -5444,6 +5450,8 @@ export class LlEmitter {
return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names);
case "regex":
return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names);
case "regexReplacer":
return addNumberedSymbol(this.regexReplacers, key, value as never, "sc_rr_", names);
case "templateStrings":
return addNumberedSymbol(
this.templateStringsInstances,
@@ -661,6 +661,29 @@ export function regexLiteralTemplate(
return `@${re.sym}`;
}
/** The build-time replacer of a global substitution of regex literal `e`
* with the literal `replacement` (regex-matchers.ts), registered on first
* use. */
export function regexLiteralReplacer(
host: LlvmEmitterContext,
e: IrExpr & { kind: "regexLit" },
replacement: string,
): string {
const key = `${e.flags}/${e.pattern}\0${replacement}`;
let rr = host.regexReplacers.get(key);
if (!rr) {
rr = {
sym: `sc_rr_${host.regexReplacers.size}`,
pattern: e.pattern,
flags: e.flags,
replacement,
};
host.regexReplacers.set(key, rr);
}
host.journal?.entry("regexReplacer", key, rr);
return `@${rr.sym}`;
}
/** A string equality or comparison between `s.charAt(i)` and a literal of
* exactly one UTF-16 code unit: the charAt call, the unit, and whether the
* charAt is the left operand. */
@@ -6,7 +6,8 @@ import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { f64Lit, llvmCommentText } from "./common.js";
import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js";
import { regexLiteralTemplate } from "./expr-primitives.js";
import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js";
import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js";
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
import { preservesRegexInputs } from "./reference-effects.js";
@@ -160,6 +161,39 @@ function directRegexOperandMethod(method: string): boolean {
}
}
/** The build-time replacer of a substitution: a regex literal running on
* its template, with a literal replacement the replacer can write as is.
* Null when the substitution takes the runtime's loop. */
function literalReplacer(
host: LlvmEmitterContext,
literal: IrExpr | null,
template: string | null,
replacement: IrExpr,
): string | null {
if (literal === null || template === null || literal.kind !== "regexLit") return null;
if (replacement.kind !== "strLit") return null;
if (!canReplaceWith(literal.pattern, literal.flags, replacement.value)) return null;
if (!regexLiteralCompiles(literal.pattern, literal.flags)) return null;
return regexLiteralReplacer(host, literal, replacement.value);
}
/** scr_regex_replace_with: the runtime runs `replacer` on an ASCII subject
* and its own loop on any other. */
function emitReplaceWith(
host: LlvmEmitterContext,
e: IrExpr,
r: LlValue,
args: LlValue[],
replacer: string,
): LlValue {
host.declare(`declare ptr @scr_regex_replace_with(ptr, ptr, ptr, ptr)`);
const t = host.B.tmp();
host.B.line(
`${t} = call ptr @scr_regex_replace_with(ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${replacer})`,
);
return host.own({ name: t, type: e.type });
}
export function emitRegexIntrinsic(
host: LlvmEmitterContext,
e: IrExpr & { kind: "regexIntrinsic" },
@@ -234,11 +268,16 @@ export function emitRegexIntrinsic(
"scr_regex_match_all_captures_into",
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
);
case "replaceAll":
case "replaceAll": {
// A global literal (the only kind a replacer is built for) never takes
// replaceAll's non-global TypeError.
const replacer = literalReplacer(host, literal, template, e.args[1]!);
if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer);
return fallible(
"scr_regex_replace_all",
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
);
}
case "split":
host.declare(`declare ptr @scr_regex_split_limit(ptr, ptr, double)`);
{
@@ -323,6 +362,8 @@ export function emitRegexIntrinsic(
return host.own({ name: t, type: e.type });
}
case "replace": {
const replacer = literalReplacer(host, literal, template, e.args[1]!);
if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer);
host.declare(`declare ptr @scr_regex_replace(ptr, ptr, ptr)`);
const t = B.tmp();
B.line(
@@ -1,5 +1,6 @@
import { describe, expect, test } from "vitest";
import {
canReplaceWith,
compileRegexMatcher,
regexLiteralBailReason,
regexLiteralCaptures,
@@ -152,3 +153,46 @@ describe("regex literal matcher code", () => {
expect(text).not.toContain("constant [128 x i8]");
});
});
describe("regex literal replacers", () => {
test("take global, non-sticky, non-plain-text patterns and ASCII replacements without $", () => {
expect(canReplaceWith("a[bc]", "g", "<2>")).toBe(true);
expect(canReplaceWith("a[bc]", "gi", "")).toBe(true);
expect(canReplaceWith("a[bc]", "", "x")).toBe(false);
expect(canReplaceWith("a[bc]", "gy", "x")).toBe(false);
expect(canReplaceWith("a[bc]", "g", "$1")).toBe(false);
expect(canReplaceWith("a[bc]", "g", "é")).toBe(false);
// Plain text is a string search at run time.
expect(canReplaceWith("&", "g", "&amp;")).toBe(false);
expect(canReplaceWith("a\\.b", "g", "x")).toBe(false);
// Letters under i are not plain text.
expect(canReplaceWith("ab", "gi", "x")).toBe(true);
});
test("append the gap and the replacement and continue the search", () => {
const code = compileRegexMatcher("r", "<[^>]*>", "g", "i64", true, "|");
const text = code!.lines.join("\n");
expect(text).toContain('@r.rep = private unnamed_addr constant [1 x i8] c"|"');
expect(text).toContain("call void @scr_re_replace_out_reserve(ptr %out, i64 %m.need)");
expect(text).toContain("load <16 x i8>, ptr %m.src, align 1");
expect(text).toMatch(/br i1 %m\.past, label %nomatch, label %scan/);
expect(text).not.toContain("m.ret:");
expect(text).toContain("ret i32 %n.found");
// The search after a match is a 16-byte scan, not a memchr call.
expect(text).not.toContain("@memchr");
expect(code!.decls).toContain("declare void @scr_re_replace_out_reserve(ptr, i64)");
// Without vectors the gap is a memcpy.
const scalar = compileRegexMatcher("r", "<[^>]*>", "g", "i64", false, "|")!.lines.join("\n");
expect(scalar).not.toContain("<16 x i8>");
expect(scalar).toContain(
"call void @llvm.memcpy.p0.p0.i64(ptr %m.dst, ptr %m.src, i64 %m.gapw, i1 false)",
);
});
test("escape the replacement bytes and skip the copy of an empty one", () => {
const quoted = compileRegexMatcher("r", "a[bc]", "g", "i64", true, '"\\\n')!.lines.join("\n");
expect(quoted).toContain('c"\\22\\5C\\0A"');
const empty = compileRegexMatcher("r", "a[bc]", "g", "i64", true, "")!.lines.join("\n");
expect(empty).not.toContain("@r.rep");
});
});
@@ -24,6 +24,8 @@
* `len`, it returns the capture count (whole match included) so the runtime
* can check it against the compiled bytecode. */
import { InternalCompilerError } from "../../errors.js";
const ASCII = 128;
const INF = -1;
/** Larger patterns keep the interpreter (binary size). */
@@ -1853,10 +1855,13 @@ export function compileRegexMatcher(
flags: string,
sizeType: string,
vector: boolean,
replacement: string | null = null,
): RegexMatcherCode | null {
const parsed = parseSubset(pattern, flags);
if (parsed === null) return null;
const sticky = flags.includes("y");
if (replacement !== null && !canReplaceWith(pattern, flags, replacement))
throw new InternalCompilerError(`llvm emitter bug: no replacer for /${pattern}/${flags}`);
const { root, ncap } = parsed;
const gen = new RxGen(sym, ncap, vector);
const end: FirstInfo = { set: emptySet(), nullable: true, bol: false, uncond: true };
@@ -1873,6 +1878,12 @@ export function compileRegexMatcher(
fn.push(` %stk = alloca ptr`);
fn.push(` %sbuf = alloca [64 x i32]`);
for (let r = 0; r < gen.regs; r++) fn.push(` %r${r} = alloca i32`);
if (replacement !== null) {
fn.push(` %next = alloca i32`);
fn.push(` %nfound = alloca i32`);
fn.push(` store i32 0, ptr %next`);
fn.push(` store i32 0, ptr %nfound`);
}
fn.push(` store ptr %sbuf, ptr %stk`);
fn.push(` store i32 64, ptr %scap`);
fn.push(` store i32 %start, ptr %at`);
@@ -1900,7 +1911,9 @@ export function compileRegexMatcher(
} else {
fn.push(` br label %seek`);
}
if (firstCount === 1) {
// A replacer seeks again after every match, often a few bytes on: the
// 16-byte scan avoids a memchr call per match.
if (firstCount === 1 && replacement === null) {
fn.push(`seek:`);
const byte = intervals(first.set)[0]![0]!;
fn.push(` %rest = sub i32 %len, %a`);
@@ -1944,6 +1957,98 @@ export function compileRegexMatcher(
fn.push(` br i1 %over, label %nomatch, label %scan`);
}
if (replacement !== null) {
emitReplaceMatch(fn, sym, replacement, vector);
} else {
emitReturnMatch(fn, ncap);
}
const result = replacement !== null ? "%n.found" : "0";
fn.push(`nomatch:`);
if (replacement !== null) {
fn.push(` %n.next = load i32, ptr %next`);
fn.push(` %n.nextp = getelementptr inbounds i8, ptr %out, i64 16`);
fn.push(` %n.nextw = sext i32 %n.next to i64`);
fn.push(` store i64 %n.nextw, ptr %n.nextp`);
fn.push(` %n.found = load i32, ptr %nfound`);
}
fn.push(` %n.stk = load ptr, ptr %stk`);
fn.push(` %n.heap = icmp ne ptr %n.stk, %sbuf`);
fn.push(` br i1 %n.heap, label %n.free, label %n.ret`);
fn.push(`n.free:`);
fn.push(` call void @scr_regex_stack_free(ptr %n.stk)`);
fn.push(` br label %n.ret`);
fn.push(`n.ret:`);
fn.push(` ret i32 ${result}`);
gen.emitTail();
for (const l of gen.body) fn.push(l);
fn.push(`}`);
const decls = REGEX_MATCHER_DECLS.slice();
if (gen.body.some((l) => l.includes("@llvm.cttz.i16")))
decls.push(`declare i16 @llvm.cttz.i16(i16, i1 immarg)`);
if (!sticky && !first.nullable && firstCount === 1 && replacement === null)
decls.push(`declare ptr @memchr(ptr, i32, ${sizeType})`);
if (replacement !== null) {
decls.push(`declare void @scr_re_replace_out_reserve(ptr, i64)`);
decls.push(`declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)`);
}
const globals = [...gen.globals];
if (replacement !== null && replacement.length > 0)
globals.push(
`@${sym}.rep = private unnamed_addr constant [${replacement.length} x i8] c"${llvmBytes(replacement)}"`,
);
return { lines: [...globals, ...fn], decls };
}
/** Whether a substitution can run through a build-time replacer: a global,
* non-sticky pattern that is not plain text (the runtime searches those as
* strings) and an ASCII replacement without `$` patterns. */
export function canReplaceWith(pattern: string, flags: string, replacement: string): boolean {
if (!flags.includes("g") || flags.includes("y") || isPlainTextPattern(pattern, flags))
return false;
for (let i = 0; i < replacement.length; i++) {
const c = replacement.charCodeAt(i);
if (c >= ASCII || c === 0x24) return false;
}
return true;
}
/** scr_regex_plain_text's rule: a pattern of literal characters and escaped
* metacharacters (no letters under `i`) that the runtime searches as text. */
function isPlainTextPattern(pattern: string, flags: string): boolean {
const fold = flags.includes("i");
if (pattern.length === 0 || pattern.length > 256) return false;
for (let i = 0; i < pattern.length; i++) {
let c = pattern[i]!;
if (c === "\\") {
if (i + 1 >= pattern.length) return false;
c = pattern[++i]!;
if (!"^$\\.*+?()[]{}|/".includes(c)) return false;
} else {
const code = c.charCodeAt(0);
if (code < 0x20 || code >= 0x7f || "^$.*+?()[]{}|".includes(c)) return false;
}
if (fold && /[a-z]/i.test(c)) return false;
}
return true;
}
function llvmBytes(text: string): string {
let out = "";
for (let i = 0; i < text.length; i++) {
const c = text.charCodeAt(i);
out +=
c >= 0x20 && c < 0x7f && c !== 0x22 && c !== 0x5c
? text[i]
: `\\${c.toString(16).toUpperCase().padStart(2, "0")}`;
}
return out;
}
/** The match block of a matcher: store the capture pointers, return 1. */
function emitReturnMatch(fn: string[], ncap: number): void {
fn.push(`match:`);
fn.push(` %m.end = load i32, ptr %pos`);
fn.push(` %m.start = load i32, ptr %at`);
@@ -1969,25 +2074,72 @@ export function compileRegexMatcher(
fn.push(` br label %m.ret`);
fn.push(`m.ret:`);
fn.push(` ret i32 1`);
}
fn.push(`nomatch:`);
fn.push(` %n.stk = load ptr, ptr %stk`);
fn.push(` %n.heap = icmp ne ptr %n.stk, %sbuf`);
fn.push(` br i1 %n.heap, label %n.free, label %n.ret`);
fn.push(`n.free:`);
fn.push(` call void @scr_regex_stack_free(ptr %n.stk)`);
fn.push(` br label %n.ret`);
fn.push(`n.ret:`);
fn.push(` ret i32 0`);
gen.emitTail();
for (const l of gen.body) fn.push(l);
fn.push(`}`);
const decls = REGEX_MATCHER_DECLS.slice();
if (gen.body.some((l) => l.includes("@llvm.cttz.i16")))
decls.push(`declare i16 @llvm.cttz.i16(i16, i1 immarg)`);
if (!sticky && !first.nullable && firstCount === 1)
decls.push(`declare ptr @memchr(ptr, i32, ${sizeType})`);
return { lines: [...gen.globals, ...fn], decls };
/** The match block of a replacer: append the gap since the previous match
* and the replacement to the output (ScrReReplaceOut: len, cap, next at
* 0, 8 and 16, then data), and continue the search at the match end, or one past it for an
* empty match, as the runtime's replace loop does. */
function emitReplaceMatch(fn: string[], sym: string, replacement: string, vector: boolean): void {
const n = replacement.length;
fn.push(`match:`);
fn.push(` %m.end = load i32, ptr %pos`);
fn.push(` %m.start = load i32, ptr %at`);
fn.push(` %m.next = load i32, ptr %next`);
fn.push(` %m.gap = sub i32 %m.start, %m.next`);
fn.push(` %m.gapw = zext i32 %m.gap to i64`);
fn.push(` %m.lenp = getelementptr inbounds i8, ptr %out, i64 0`);
fn.push(` %m.capp = getelementptr inbounds i8, ptr %out, i64 8`);
fn.push(` %m.datap = getelementptr inbounds i8, ptr %out, i64 24`);
fn.push(` %m.len0 = load i64, ptr %m.lenp`);
fn.push(` %m.cap = load i64, ptr %m.capp`);
fn.push(` %m.room = sub i64 %m.cap, %m.len0`);
// 16 bytes of slack past the gap and the replacement for the 16-byte copy.
fn.push(` %m.need = add i64 %m.gapw, ${n + 32}`);
fn.push(` %m.fits = icmp uge i64 %m.room, %m.need`);
fn.push(` br i1 %m.fits, label %m.put, label %m.grow`);
fn.push(`m.grow:`);
fn.push(` call void @scr_re_replace_out_reserve(ptr %out, i64 %m.need)`);
fn.push(` br label %m.put`);
fn.push(`m.put:`);
fn.push(` %m.data = load ptr, ptr %m.datap`);
fn.push(` %m.len = load i64, ptr %m.lenp`);
fn.push(` %m.dst = getelementptr inbounds i8, ptr %m.data, i64 %m.len`);
fn.push(` %m.nextw = sext i32 %m.next to i64`);
fn.push(` %m.src = getelementptr inbounds i8, ptr %s, i64 %m.nextw`);
if (vector) {
// A gap of at most 16 bytes with 16 readable subject bytes is one
// unaligned 16-byte copy.
fn.push(` %m.small = icmp ule i32 %m.gap, 16`);
fn.push(` %m.rest = sub i32 %len, %m.next`);
fn.push(` %m.readable = icmp sge i32 %m.rest, 16`);
fn.push(` %m.fast = and i1 %m.small, %m.readable`);
fn.push(` br i1 %m.fast, label %m.copy16, label %m.copy`);
fn.push(`m.copy16:`);
fn.push(` %m.v = load <16 x i8>, ptr %m.src, align 1`);
fn.push(` store <16 x i8> %m.v, ptr %m.dst, align 1`);
fn.push(` br label %m.rep`);
} else {
fn.push(` br label %m.copy`);
}
fn.push(`m.copy:`);
fn.push(` call void @llvm.memcpy.p0.p0.i64(ptr %m.dst, ptr %m.src, i64 %m.gapw, i1 false)`);
fn.push(` br label %m.rep`);
fn.push(`m.rep:`);
fn.push(` %m.rdst = getelementptr inbounds i8, ptr %m.dst, i64 %m.gapw`);
if (n > 0)
fn.push(` call void @llvm.memcpy.p0.p0.i64(ptr %m.rdst, ptr @${sym}.rep, i64 ${n}, i1 false)`);
fn.push(` %m.len1 = add i64 %m.len, %m.gapw`);
fn.push(` %m.len2 = add i64 %m.len1, ${n}`);
fn.push(` store i64 %m.len2, ptr %m.lenp`);
fn.push(` store i32 %m.end, ptr %next`);
fn.push(` %m.found0 = load i32, ptr %nfound`);
fn.push(` %m.found1 = add i32 %m.found0, 1`);
fn.push(` store i32 %m.found1, ptr %nfound`);
fn.push(` %m.empty = icmp eq i32 %m.end, %m.start`);
fn.push(` %m.after = add i32 %m.end, 1`);
fn.push(` %m.at = select i1 %m.empty, i32 %m.after, i32 %m.end`);
fn.push(` store i32 %m.at, ptr %at`);
fn.push(` %m.past = icmp sgt i32 %m.at, %len`);
fn.push(` br i1 %m.past, label %nomatch, label %scan`);
}
@@ -0,0 +1,42 @@
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);
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8351-regex-literal-replacers.ts"
],
"diags": []
}
+45
View File
@@ -1416,6 +1416,51 @@ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep) {
return scr_replace_impl(s, re, rep);
}
void scr_re_replace_out_reserve(ScrReReplaceOut *o, int64_t need) {
if (need < 0 || (uint64_t)need > SCR_STR_MAX_CAP - (uint64_t)o->len) scr_regex_oom();
size_t cap = (size_t)o->cap * 2;
if (cap < (size_t)(o->len + need)) cap = (size_t)(o->len + need);
if (cap > SCR_STR_MAX_CAP) cap = SCR_STR_MAX_CAP;
o->str->len = (size_t)o->len;
o->str = scr_str_regrow(o->str, cap);
o->data = o->str->data;
o->cap = (int64_t)o->str->cap;
}
/* A global substitution with a literal ASCII replacement through the
* build-time replacer of its (pattern, replacement) pair: on an ASCII
* subject the replacer writes each gap and the replacement into the output
* and continues the search after every match without returning. Every
* other subject, and a plain-text pattern (string search), takes
* scr_replace_impl. */
ScrStr *scr_regex_replace_with(ScrStr *s, ScrRegex *re, ScrStr *rep,
ScrRegexMatcherFn replacer) {
(void)scr_regex_bc(re);
if (scr_regex_lit(re) || !scr_text_ascii(s)) return scr_replace_impl(s, re, rep);
if (s->len > (size_t)INT32_MAX - 1) scr_regex_oom();
re->last_index = 0;
ScrReReplaceOut o;
o.str = scr_str_alloc_raw(0, s->len + 64);
o.data = o.str->data;
o.len = 0;
o.cap = (int64_t)o.str->cap;
o.next = 0;
int32_t n = replacer((const uint8_t *)s->data, (int32_t)s->len, 0, (uint8_t **)(void *)&o);
if (n == 0) {
o.str->len = 0;
scr_str_release(o.str);
return scr_str_retain(s);
}
size_t tail = s->len - (size_t)o.next;
if ((int64_t)(tail + 1) > o.cap - o.len) scr_re_replace_out_reserve(&o, (int64_t)tail + 1);
scr_copy_bytes(o.data + o.len, s->data + o.next, tail);
o.len += (int64_t)tail;
o.str->len = (size_t)o.len;
o.data[o.len] = '\0';
scr_str_note_ascii(o.str);
return o.str;
}
typedef struct {
ScrDyn *arguments;
int start, end;
+16
View File
@@ -80,6 +80,22 @@ ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */
ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */
/* replace: first match without /g, every match with it. */
ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep);
/* The output of a build-time replacer (regex-matchers.ts): the replacer
* appends each gap and the replacement at data[len] after reserving room,
* and leaves in `next` the subject index the unmatched tail starts at. */
typedef struct {
int64_t len;
int64_t cap;
int64_t next;
char *data; /* at offset 24 on every target */
ScrStr *str;
} ScrReReplaceOut;
/* Grows o->str so that `need` more bytes fit at o->data[o->len]. */
void scr_re_replace_out_reserve(ScrReReplaceOut *o, int64_t need);
/* A global substitution with a literal ASCII replacement, through the
* replacer built for that pattern and replacement. Borrows all. */
ScrStr *scr_regex_replace_with(ScrStr *s, ScrRegex *re, ScrStr *rep,
ScrRegexMatcherFn replacer);
/* replace/replaceAll with a replacer closure of `arity` (at most
* SCR_REPLACER_MAX_ARITY) parameters. Parameter i receives what Node passes
* at position i: the match and captures below the capture count n, the
@@ -0,0 +1,72 @@
// Global substitutions of regex literals with literal replacements: matches
// at every offset around 16-character blocks and at the end of the
// subject, empty matches, alternation order, case-insensitive and
// multiline flags, word boundaries, replacements that are empty, longer
// than the match or hold quotes and backslashes, outputs that grow far past
// the subject, subjects without a match, and subjects outside ASCII (which
// take the general loop). replace and replaceAll.
const substitutions: [string, (s: string) => string][] = [
["<[^>]*>", (s) => s.replace(/<[^>]*>/g, "|")],
["a[NSt]|BY", (s) => s.replace(/a[NSt]|BY/g, "<2>")],
["tHa[Nt]", (s) => s.replace(/tHa[Nt]/g, "<4>")],
["\\|[^|][^|]*\\|", (s) => s.replace(/\|[^|][^|]*\|/g, "-")],
["x*", (s) => s.replace(/x*/g, "-")],
["\\b", (s) => s.replace(/\b/g, "^")],
["^ab", (s) => s.replace(/^ab/gm, "Z")],
["ab|abc|b", (s) => s.replace(/ab|abc|b/g, "#")],
["AB (i)", (s) => s.replace(/AB/gi, "")],
["(a)(b)?", (s) => s.replace(/(a)(b)?/g, "q")],
["\\d+", (s) => s.replace(/\d+/g, '"\\\n')],
["[^a-z]", (s) => s.replace(/[^a-z]/g, "")],
["a", (s) => s.replace(/a/g, "<<<<a>>>>")],
["cat (replaceAll)", (s) => s.replaceAll(/cat/g, "dog")],
["[aeiou]{2} (replaceAll)", (s) => s.replaceAll(/[aeiou]{2}/g, "~")],
];
function hash(text: string, h: number): number {
for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0;
return h;
}
const filler = "qwertyuiop asdfghjkl zxcvbnm";
const needles = ["<2>", "<abc>", "aN", "BY", "tHat", "|ab|", "||", "xx", "ab", "abc", "AB", "123", "cat", "aeio", "a"];
let checksum = 0;
let lines = 0;
for (let length = 0; length <= 70; length += 7) {
for (const needle of needles) {
for (const offset of [0, 1, 14, 15, 16, 17, 31, 32, 33]) {
if (offset + needle.length > length) continue;
let subject = "";
while (subject.length < length) subject += filler;
subject = subject.slice(0, offset) + needle + subject.slice(offset + needle.length, length);
for (const [name, sub] of substitutions) {
const out = sub(subject);
checksum = hash(out, checksum);
lines++;
if (length === 35 && (offset === 15 || offset === 16)) console.log(name, offset, JSON.stringify(out));
}
}
}
}
console.log("lines", lines, "checksum", checksum);
// Dense matches, matches that end the subject, and an output many times
// larger than the subject.
const dense = "<1><22><333>".repeat(40) + "tail<x>";
for (const [name, sub] of substitutions) console.log(name, JSON.stringify(sub(dense).slice(-40)), sub(dense).length);
const many = "a".repeat(5000);
console.log("grow", many.replace(/a/g, "<<<<a>>>>").length, many.replace(/a/g, "<<<<a>>>>").slice(4990));
console.log("empty", JSON.stringify("".replace(/x*/g, "-")), JSON.stringify("".replace(/a/g, "b")));
console.log("none", "zzzz".replace(/a[bc]/g, "!"), "zzzz".replaceAll(/a[bc]/g, "!"));
// Subjects outside ASCII run the general loop with the same results.
for (const subject of ["héllo <x> wörld <yy>", "日本<語>", "a\u{1F600}b<c>", "cat café cat"]) {
console.log(JSON.stringify(subject.replace(/<[^>]*>/g, "|")), JSON.stringify(subject.replaceAll(/cat/g, "dog")), JSON.stringify(subject.replace(/x*/g, "-")));
}
// A long DNA-like subject, as in regex-redux.
let dna = "";
for (let i = 0; i < 4000; i++) dna += "acgt"[(i * 7 + (i >> 2)) % 4] + (i % 61 === 0 ? "tHaN" : i % 37 === 0 ? "aNBY" : "");
const step1 = dna.replace(/tHa[Nt]/g, "<4>").replace(/a[NSt]|BY/g, "<2>");
const step2 = step1.replace(/<[^>]*>/g, "|").replace(/\|[^|][^|]*\|/g, "-");
console.log("dna", dna.length, step1.length, step2.length, hash(step2, 0));
@@ -0,0 +1 @@
{ "plain": 3074, "san": 44176 }