mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
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.
This commit is contained in:
@@ -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
|
||||
@@ -5439,6 +5445,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", "&")).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);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8351-regex-literal-replacers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user