mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Run global substitutions with a literal replacement through build-time replacers (#2240)
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
|
||||
@@ -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", "&")).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