mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Call replace callbacks with unannotated parameters directly
TypeScript types the parameters of an inline replace callback after the match `any`. An unannotated parameter that is never reassigned, whose every read means the same for a string or number as for `any`, and that receives a capture every match sets, the offset or the subject now takes that value's type. String-search callbacks get the same treatment for the offset and the subject. scr_regex_replace_fn calls the closure directly in more cases: - checked-dynamic parameters, which receive the boxed value Node passes at their position (a capture or undefined, the offset, the subject, the groups object, then undefined); - `string | undefined` capture parameters, which receive NULL when the match leaves the capture unset; - a checked-dynamic result, converted with ToString; - up to six parameters. Callbacks of the earlier typed form compile to the same call, with a kinds argument of 0, and run the same per-match work.
This commit is contained in:
@@ -30,6 +30,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
|
||||
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset |
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/regex-replacers.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Regex replace and replaceAll with inline callbacks, written as they
|
||||
// usually are, without parameter annotations: template placeholders looked
|
||||
// up in a dictionary, snake_case to camelCase, date reordering, optional
|
||||
// version parts, capitalization and a string search with its offset.
|
||||
let seed = 7;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function pick<T>(items: readonly T[]): T {
|
||||
return items[Math.floor(random() * items.length)]!;
|
||||
}
|
||||
|
||||
const words = ["user", "order", "item", "cart", "price", "total", "status", "created", "updated", "id"];
|
||||
const names = ["Ada", "Grace", "Linus", "Barbara", "Ken", "Margaret"];
|
||||
const vars: Record<string, string> = {
|
||||
name: "world",
|
||||
city: "Lisbon",
|
||||
day: "Tuesday",
|
||||
plan: "pro",
|
||||
count: "12",
|
||||
};
|
||||
const keys = [...Object.keys(vars), "missing"];
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const count = Math.floor(20000 * scale);
|
||||
const templates: string[] = [];
|
||||
const identifiers: string[] = [];
|
||||
const dates: string[] = [];
|
||||
const versions: string[] = [];
|
||||
const sentences: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
templates.push(`Hello {${pick(keys)}}, your {${pick(keys)}} plan renews on {${pick(keys)}} in {${pick(keys)}}.`);
|
||||
identifiers.push(`${pick(words)}_${pick(words)}_${pick(words)}${random() < 0.5 ? "_" + pick(words) : ""}`);
|
||||
const year = 1990 + Math.floor(random() * 40);
|
||||
const month = String(1 + Math.floor(random() * 12)).padStart(2, "0");
|
||||
const day = String(1 + Math.floor(random() * 28)).padStart(2, "0");
|
||||
dates.push(`from ${year}-${month}-${day} to ${year + 1}-${month}-${day} (${pick(names)})`);
|
||||
versions.push(`v${i % 9}.${i % 13}${random() < 0.5 ? "." + (i % 7) : ""} and v${i % 5}.${i % 3}`);
|
||||
sentences.push(`${pick(names)} said the ${pick(words)} and the ${pick(words)} were ready for ${pick(names)}`);
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
let checksum = 0;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const filled = templates[i]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`);
|
||||
const camel = identifiers[i]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
||||
const reordered = dates[i]!.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, m, d) => `${d}/${m}/${y}`);
|
||||
const padded = versions[i]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`);
|
||||
const titled = sentences[i]!.replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
||||
const marked = sentences[i]!.replaceAll("the", (m, at) => (at % 2 === 0 ? m.toUpperCase() : m));
|
||||
length += filled.length + camel.length + reordered.length + padded.length + titled.length + marked.length;
|
||||
checksum = (checksum * 31 + camel.charCodeAt(camel.length - 1) + padded.charCodeAt(padded.length - 1)) >>> 0;
|
||||
}
|
||||
}
|
||||
console.log(templates[0]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
|
||||
console.log(identifiers[1]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
|
||||
console.log(versions[2]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`));
|
||||
console.log("length", length, "checksum", checksum);
|
||||
@@ -843,35 +843,68 @@ function emitAssertFailing(
|
||||
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
||||
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
||||
|
||||
/** The parameter count of a replace callback the runtime can call through
|
||||
* the closure ABI (scr_regex_replace_fn), or null when it needs the dyn
|
||||
* box. Each declared parameter must receive a value of its declared kind
|
||||
* in every match: the match string at 0; with more than one parameter the
|
||||
/** The most parameters scr_regex_replace_fn calls a replacer with
|
||||
* (SCR_REPLACER_MAX_ARITY), the first bit of its string-or-undefined
|
||||
* parameters (SCR_REPLACER_OPTIONAL), and its flag for a checked-dynamic
|
||||
* result (SCR_REPLACER_BOXED_RESULT). */
|
||||
const REPLACER_MAX_ARITY = 6;
|
||||
const REPLACER_OPTIONAL = 2 ** 8;
|
||||
const REPLACER_BOXED_RESULT = 2 ** 31;
|
||||
|
||||
/** `string | undefined` held as the string pointer, NULL for undefined. */
|
||||
function optionalStringType(host: LlvmEmitterContext, type: IrType): boolean {
|
||||
if (type.kind !== "union") return false;
|
||||
const nullable = host.nullableUnions.get(type.unionId);
|
||||
if (nullable?.repr !== "single" || nullable.arm.kind !== "string" || nullable.nullTag >= 0)
|
||||
return false;
|
||||
return host.unionsById.get(type.unionId)?.arms[nullable.unitTag]?.kind === "undefinedT";
|
||||
}
|
||||
|
||||
/** The parameter count and kinds (scr_regex_replace_fn's `kinds`) of a
|
||||
* replace callback the runtime can call through the closure ABI, or null
|
||||
* when it needs the dyn box. A checked-dynamic parameter receives whatever
|
||||
* Node passes at its position. Each typed parameter must receive a value of
|
||||
* its declared kind in every match: the match string at 0; past 0 the
|
||||
* pattern must be a literal whose captures are known, so position i is a
|
||||
* capture that every match sets (a string), the offset (a number) right
|
||||
* after the captures, or the subject (a string) after that. */
|
||||
function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
* capture (a string when every match sets it, otherwise `string |
|
||||
* undefined`), the offset (a number) right after the captures, or the
|
||||
* subject (a string) after that. The result is a string or a
|
||||
* checked-dynamic value. */
|
||||
function typedReplacerShape(
|
||||
host: LlvmEmitterContext,
|
||||
callback: IrType,
|
||||
pattern: IrExpr,
|
||||
): { arity: number; kinds: number } | null {
|
||||
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
||||
if (callback.ret.kind !== "string") return null;
|
||||
if (callback.ret.kind !== "string" && callback.ret.kind !== "dyn") return null;
|
||||
const params = callback.params;
|
||||
if (params.length > 4) return null;
|
||||
if (params.length === 0) return 0;
|
||||
if (params[0]!.kind !== "string") return null;
|
||||
if (params.length === 1) return 1;
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
const captures = regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
for (let i = 1; i < params.length; i++) {
|
||||
if (params.length > REPLACER_MAX_ARITY) return null;
|
||||
let kinds = callback.ret.kind === "dyn" ? REPLACER_BOXED_RESULT : 0;
|
||||
let captures: { count: number; always: boolean[] } | null | undefined;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const kind = params[i]!.kind;
|
||||
if (kind === "dyn") {
|
||||
kinds += 2 ** i;
|
||||
continue;
|
||||
}
|
||||
const optional = optionalStringType(host, params[i]!);
|
||||
if (i === 0) {
|
||||
if (kind !== "string") return null;
|
||||
continue;
|
||||
}
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
captures ??= regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
const ok =
|
||||
i < captures.count
|
||||
? kind === "string" && captures.always[i] === true
|
||||
? (kind === "string" && captures.always[i] === true) || optional
|
||||
: i === captures.count
|
||||
? kind === "f64"
|
||||
: i === captures.count + 1 && kind === "string";
|
||||
if (!ok) return null;
|
||||
if (optional) kinds += REPLACER_OPTIONAL * 2 ** i;
|
||||
}
|
||||
return params.length;
|
||||
return { arity: params.length, kinds };
|
||||
}
|
||||
|
||||
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
||||
@@ -881,16 +914,16 @@ function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const [subject, pattern, callback, all] = e.args;
|
||||
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
||||
const arity = typedReplacerArity(callback.value.type, pattern);
|
||||
if (arity === null) return null;
|
||||
const shape = typedReplacerShape(host, callback.value.type, pattern);
|
||||
if (shape === null) return null;
|
||||
const B = host.B;
|
||||
const s = host.emitExpr(subject);
|
||||
const re = host.emitExpr(pattern);
|
||||
const fn = host.emitExpr(callback.value);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i32, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`,
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${shape.arity}, i32 ${shape.kinds | 0}, i1 zeroext ${all.value})`,
|
||||
);
|
||||
const output = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { regexCaptureParticipation } from "./replacer-params.js";
|
||||
|
||||
describe("regex capture participation", () => {
|
||||
test("counts captures and marks the ones every match sets", () => {
|
||||
expect(regexCaptureParticipation("\\{(\\w+)\\}", "g")).toEqual({
|
||||
count: 2,
|
||||
always: [true, true],
|
||||
});
|
||||
expect(regexCaptureParticipation("(\\d{4})-(\\d{2})(?:-(\\d{2}))?", "g")).toEqual({
|
||||
count: 4,
|
||||
always: [true, true, true, false],
|
||||
});
|
||||
expect(regexCaptureParticipation("(a)|(b)", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, false, false],
|
||||
});
|
||||
expect(regexCaptureParticipation("((a)|b)", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, true, false],
|
||||
});
|
||||
expect(regexCaptureParticipation("(?:(a)|b)(c)", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, false, true],
|
||||
});
|
||||
expect(regexCaptureParticipation("(a)*(b)+(c){0,2}(d){1,3}(e){00}", "")).toEqual({
|
||||
count: 6,
|
||||
always: [true, false, true, false, true, false],
|
||||
});
|
||||
expect(regexCaptureParticipation("(?=(a))(?!(b))(?<=(c))(?<!(d))(?<name>e)", "")).toEqual({
|
||||
count: 6,
|
||||
always: [true, false, false, false, false, true],
|
||||
});
|
||||
});
|
||||
|
||||
test("skips escapes, classes and non-capturing groups", () => {
|
||||
expect(regexCaptureParticipation("\\((a)\\)[(|)\\]](?:b)(?i:c)", "")).toEqual({
|
||||
count: 2,
|
||||
always: [true, true],
|
||||
});
|
||||
expect(regexCaptureParticipation("[](a)", "")).toEqual({ count: 2, always: [true, true] });
|
||||
expect(regexCaptureParticipation("(a)[", "")).toBeNull();
|
||||
expect(regexCaptureParticipation("(a", "")).toBeNull();
|
||||
expect(regexCaptureParticipation("a)", "")).toBeNull();
|
||||
expect(regexCaptureParticipation("(a)", "v")).toBeNull();
|
||||
});
|
||||
test("marks captures in alternations, optional groups and lookarounds", () => {
|
||||
const always = (pattern: string): boolean[] | null =>
|
||||
regexCaptureParticipation(pattern, "")?.always ?? null;
|
||||
expect(always("tag(\\d+)")).toEqual([true, true]);
|
||||
expect(always("((a)b)+c")).toEqual([true, true, true]);
|
||||
expect(always("(?<name>\\d)(x)")).toEqual([true, true, true]);
|
||||
expect(always("(a){2,3}")).toEqual([true, true]);
|
||||
expect(always("(?:x|y)(a)")).toEqual([true, true]);
|
||||
expect(always("(?:(a)|b)c")).toEqual([true, false]);
|
||||
expect(always("(x)?(\\d)")).toEqual([true, false, true]);
|
||||
expect(always("(?:(t)a)*g")).toEqual([true, false]);
|
||||
expect(always("((a)?)")).toEqual([true, true, false]);
|
||||
expect(always("(?<=(-))(\\w)")).toEqual([true, false, true]);
|
||||
expect(always("[\\]|(](b)")).toEqual([true, true]);
|
||||
expect(always("[^)](c)\\|")).toEqual([true, true]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { F64, STRING, type IrExpr, type IrType } from "../../../ir/ir.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { bindingNeverReassigned } from "../binding-analysis.js";
|
||||
|
||||
/** The captures of a regex literal's pattern (the whole match included)
|
||||
* and, per capture, whether every successful match sets it. A capture may
|
||||
* be unset when an enclosing group has alternatives, when an enclosing
|
||||
* group or the capture itself is quantified with a minimum of zero, or
|
||||
* when it sits inside a lookaround. Null for the `v` flag (nested
|
||||
* classes) and for a pattern the scan does not finish. */
|
||||
export function regexCaptureParticipation(
|
||||
pattern: string,
|
||||
flags: string,
|
||||
): { count: number; always: boolean[] } | null {
|
||||
if (flags.includes("v")) return null;
|
||||
const always: boolean[] = [true];
|
||||
/** Open groups: the group's own capture (-1 when it does not capture),
|
||||
* the captures nested in it, whether it has alternatives, and whether it
|
||||
* is a lookaround. */
|
||||
const frames: { own: number; nested: number[]; alternatives: boolean; lookaround: boolean }[] = [
|
||||
{ own: -1, nested: [], alternatives: false, lookaround: false },
|
||||
];
|
||||
let i = 0;
|
||||
while (i < pattern.length) {
|
||||
const c = pattern[i]!;
|
||||
if (c === "\\") {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === "[") {
|
||||
// The first unescaped `]` ends the class (`[]` is an empty class).
|
||||
i++;
|
||||
while (i < pattern.length && pattern[i] !== "]") i += pattern[i] === "\\" ? 2 : 1;
|
||||
if (i >= pattern.length) return null;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === "|") {
|
||||
frames[frames.length - 1]!.alternatives = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === "(") {
|
||||
let capture = false;
|
||||
let lookaround = false;
|
||||
if (pattern[i + 1] !== "?") {
|
||||
capture = true;
|
||||
i++;
|
||||
} else if (pattern[i + 2] === "<" && pattern[i + 3] !== "=" && pattern[i + 3] !== "!") {
|
||||
const gt = pattern.indexOf(">", i + 3);
|
||||
if (gt < 0) return null;
|
||||
capture = true;
|
||||
i = gt + 1;
|
||||
} else {
|
||||
const kind = pattern[i + 2] === "<" ? pattern[i + 3] : pattern[i + 2];
|
||||
lookaround = kind === "=" || kind === "!";
|
||||
i += 2;
|
||||
}
|
||||
frames.push({
|
||||
own: capture ? always.length : -1,
|
||||
nested: [],
|
||||
alternatives: false,
|
||||
lookaround,
|
||||
});
|
||||
if (capture) always.push(true);
|
||||
continue;
|
||||
}
|
||||
if (c === ")") {
|
||||
if (frames.length === 1) return null;
|
||||
const frame = frames.pop()!;
|
||||
i++;
|
||||
const next = pattern[i];
|
||||
const optional =
|
||||
next === "?" || next === "*" || (next === "{" && /^\{0+[,}]/.test(pattern.slice(i)));
|
||||
const parent = frames[frames.length - 1]!;
|
||||
// Alternatives inside the group leave its nested captures unset, but
|
||||
// not the group's own capture.
|
||||
for (const index of frame.nested) {
|
||||
if (optional || frame.lookaround || frame.alternatives) always[index] = false;
|
||||
parent.nested.push(index);
|
||||
}
|
||||
if (frame.own >= 0) {
|
||||
if (optional || frame.lookaround) always[frame.own] = false;
|
||||
parent.nested.push(frame.own);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (frames.length !== 1) return null;
|
||||
if (frames[0]!.alternatives) for (const index of frames[0]!.nested) always[index] = false;
|
||||
return { count: always.length, always };
|
||||
}
|
||||
|
||||
/** String members a refined parameter may use: these lower from the
|
||||
* receiver's type whatever the checker resolved for an `any` receiver. */
|
||||
const STRING_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"length",
|
||||
"toUpperCase",
|
||||
"toLowerCase",
|
||||
"trim",
|
||||
"charCodeAt",
|
||||
"codePointAt",
|
||||
"slice",
|
||||
"substring",
|
||||
"repeat",
|
||||
"padStart",
|
||||
"padEnd",
|
||||
"includes",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
"indexOf",
|
||||
]);
|
||||
|
||||
/** Whether every read of `symbol` in `body` is one that means the same
|
||||
* for an `any` value and for a `kind` value, and lowers for both: a
|
||||
* template span, a Number/String/parseInt/parseFloat argument, a listed
|
||||
* string member, `+`, strict equality, arithmetic and comparisons of
|
||||
* numbers, a returned value, and a string key of an element access on a
|
||||
* string-indexed record or an `any` value. Anything else keeps the
|
||||
* parameter `any`. */
|
||||
function readsFit(
|
||||
lowerer: Lowerer,
|
||||
body: ts.Node,
|
||||
symbol: ts.Symbol,
|
||||
kind: "string" | "f64",
|
||||
): boolean {
|
||||
const name = ts.symbolName(symbol);
|
||||
const primitive = (e: ts.Expression): boolean => {
|
||||
const flags = lowerer.checker.getTypeAtLocation(e).flags;
|
||||
return (flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.StringLike)) !== 0;
|
||||
};
|
||||
const fits = (id: ts.Identifier): boolean => {
|
||||
let node: ts.Node = id;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isTemplateSpan(parent) || ts.isReturnStatement(parent)) return true;
|
||||
if (ts.isElementAccessExpression(parent)) {
|
||||
if (kind !== "string" || parent.argumentExpression !== node) return false;
|
||||
const receiver = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(parent.expression));
|
||||
if (receiver?.kind === "dyn") return true;
|
||||
const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined;
|
||||
return shape?.indexValue !== undefined && !shape.tuple;
|
||||
}
|
||||
if (ts.isArrowFunction(parent)) return parent.body === node;
|
||||
if (ts.isCallExpression(parent))
|
||||
return (
|
||||
parent.expression !== node &&
|
||||
["Number", "String", "parseInt", "parseFloat"].some((global) =>
|
||||
lowerer.isStdlibGlobal(parent.expression, global),
|
||||
)
|
||||
);
|
||||
if (ts.isPropertyAccessExpression(parent)) {
|
||||
const member = parent.name.text;
|
||||
if (kind !== "string" || parent.expression !== node || !STRING_MEMBERS.has(member))
|
||||
return false;
|
||||
return (
|
||||
member === "length" ||
|
||||
(ts.isCallExpression(parent.parent) && parent.parent.expression === parent)
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
const op = parent.operatorToken.kind;
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
)
|
||||
return true;
|
||||
const other = parent.left === node ? parent.right : parent.left;
|
||||
if (op === ts.SyntaxKind.PlusToken) return kind === "string" || primitive(other);
|
||||
return (
|
||||
kind === "f64" &&
|
||||
(op === ts.SyntaxKind.MinusToken ||
|
||||
op === ts.SyntaxKind.AsteriskToken ||
|
||||
op === ts.SyntaxKind.SlashToken ||
|
||||
op === ts.SyntaxKind.PercentToken ||
|
||||
op === ts.SyntaxKind.LessThanToken ||
|
||||
op === ts.SyntaxKind.GreaterThanToken ||
|
||||
op === ts.SyntaxKind.LessThanEqualsToken ||
|
||||
op === ts.SyntaxKind.GreaterThanEqualsToken) &&
|
||||
(lowerer.checker.getTypeAtLocation(other).flags & ts.TypeFlags.NumberLike) !== 0
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
let ok = true;
|
||||
const visit = (n: ts.Node): void => {
|
||||
if (!ok) return;
|
||||
if (ts.isIdentifier(n) && n.text === name && lowerer.resolveValueSymbol(n) === symbol) {
|
||||
if (!fits(n)) ok = false;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(n, visit);
|
||||
};
|
||||
visit(body);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** The capture layout of a string search: the match, then the offset and
|
||||
* the subject. */
|
||||
export const PLAIN_SEARCH_CAPTURES: { count: number; always: boolean[] } = {
|
||||
count: 1,
|
||||
always: [true],
|
||||
};
|
||||
|
||||
/** The captures of a regex literal argument (regexCaptureParticipation),
|
||||
* or null for any other pattern expression. */
|
||||
export function regexLiteralCapturesOf(
|
||||
pattern: ts.Expression,
|
||||
): { count: number; always: boolean[] } | null {
|
||||
let regex = pattern;
|
||||
while (ts.isParenthesizedExpression(regex)) regex = regex.expression;
|
||||
if (!ts.isRegularExpressionLiteral(regex)) return null;
|
||||
const text = regex.text;
|
||||
const slash = text.lastIndexOf("/");
|
||||
return regexCaptureParticipation(text.slice(1, slash), text.slice(slash + 1));
|
||||
}
|
||||
|
||||
/** Lowers an inline replace callback. JavaScript passes it the match, the
|
||||
* captures, the match offset and the subject (`captures` gives their
|
||||
* layout; null lowers the callback as written). TypeScript types the
|
||||
* parameters after the match `any`, so an unannotated parameter that is
|
||||
* never reassigned, whose every read fits its value (readsFit), and that
|
||||
* receives a capture every match sets, the offset or the subject takes
|
||||
* that value's type: a string or a number. The closure can then be called
|
||||
* directly (typed replacers in lib-dispatch.ts) with fewer boxed values. */
|
||||
export function lowerReplacerCallback(
|
||||
lowerer: Lowerer,
|
||||
callback: ts.Expression,
|
||||
captures: { count: number; always: boolean[] } | null,
|
||||
): IrExpr {
|
||||
let node = callback;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
// A named function expression can call itself with other arguments.
|
||||
const literal =
|
||||
ts.isArrowFunction(node) || (ts.isFunctionExpression(node) && !node.name) ? node : null;
|
||||
if (!literal || !captures) return lowerer.lowerExpr(callback);
|
||||
const bound: { symbol: ts.Symbol; previous: IrType | undefined }[] = [];
|
||||
const previousImplicit = lowerer.implicitParamTypes;
|
||||
let implicit: Map<ts.Symbol, ts.Type> | null = null;
|
||||
const params = literal.parameters.nodes;
|
||||
// A declared `this` parameter takes no argument position.
|
||||
const first =
|
||||
params[0] && ts.isIdentifier(params[0].name) && params[0].name.text === "this" ? 1 : 0;
|
||||
params.slice(first).forEach((param, i) => {
|
||||
const type =
|
||||
i === 0 || i > captures.count + 1
|
||||
? null
|
||||
: i < captures.count
|
||||
? captures.always[i] === true
|
||||
? STRING
|
||||
: null
|
||||
: i === captures.count
|
||||
? F64
|
||||
: STRING;
|
||||
if (
|
||||
type === null ||
|
||||
!ts.isIdentifier(param.name) ||
|
||||
param.type ||
|
||||
param.initializer ||
|
||||
param.dotDotDotToken ||
|
||||
param.questionToken
|
||||
)
|
||||
return;
|
||||
if ((lowerer.checker.getTypeAtLocation(param.name).flags & ts.TypeFlags.Any) === 0) return;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (
|
||||
!symbol ||
|
||||
!bindingNeverReassigned(lowerer, symbol, param) ||
|
||||
!readsFit(lowerer, literal.body, symbol, type === STRING ? "string" : "f64")
|
||||
)
|
||||
return;
|
||||
bound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) });
|
||||
lowerer.runtimeOptionalBindingTypes.set(symbol, type);
|
||||
implicit ??= new Map(previousImplicit ?? []);
|
||||
implicit.set(
|
||||
symbol,
|
||||
type === STRING ? lowerer.checker.getStringType() : lowerer.checker.getNumberType(),
|
||||
);
|
||||
});
|
||||
if (implicit !== null) lowerer.implicitParamTypes = implicit;
|
||||
try {
|
||||
return lowerer.lowerExpr(callback);
|
||||
} finally {
|
||||
for (const { symbol, previous } of bound) {
|
||||
if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol);
|
||||
else lowerer.runtimeOptionalBindingTypes.set(symbol, previous);
|
||||
}
|
||||
lowerer.implicitParamTypes = previousImplicit;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
|
||||
import { STRING_INDEX_METHODS, STRING_REPLACE_METHODS, STR_METHODS } from "../surfaces.js";
|
||||
import { execResultCaptureMask } from "./exec-result-uses.js";
|
||||
import { lowerStringReplacement } from "./string-replacement.js";
|
||||
import { lowerReplacerCallback, regexLiteralCapturesOf } from "./replacer-params.js";
|
||||
import {
|
||||
coerceStringSearchValue,
|
||||
defaultAfterUndefined,
|
||||
@@ -1025,7 +1026,11 @@ export function lowerRegexMethodCall(
|
||||
: [
|
||||
lowerer.lowerExprExpecting(arg0, { kind: "regex" }),
|
||||
call.arguments.nodes[1]
|
||||
? lowerer.lowerExpr(call.arguments.nodes[1])
|
||||
? lowerReplacerCallback(
|
||||
lowerer,
|
||||
call.arguments.nodes[1],
|
||||
regexLiteralCapturesOf(arg0),
|
||||
)
|
||||
: dynUndefinedExpr(loc),
|
||||
];
|
||||
if (name !== "split") {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { PLAIN_SEARCH_CAPTURES, lowerReplacerCallback } from "./replacer-params.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
coerceStringSearchValue,
|
||||
@@ -48,11 +49,18 @@ export function lowerStringReplacement(
|
||||
type.kind === "bool" ||
|
||||
type.kind === "bigint" ||
|
||||
isUnitType(type);
|
||||
const argument = (node: ts.Expression | undefined, callback: boolean): IrExpr => {
|
||||
const argument = (
|
||||
node: ts.Expression | undefined,
|
||||
callback: boolean,
|
||||
plainSearch = false,
|
||||
): IrExpr => {
|
||||
if (!node) return strLit("undefined", loc);
|
||||
const absent = lowerStaticallyUndefinedArgument(lowerer, node);
|
||||
if (absent) return defaultAfterUndefined(absent, strLit("undefined", loc));
|
||||
const value = lowerer.lowerExpr(node);
|
||||
// A plain search passes (match, offset, subject).
|
||||
const value = plainSearch
|
||||
? lowerReplacerCallback(lowerer, node, PLAIN_SEARCH_CAPTURES)
|
||||
: lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "dyn" || (callback && value.type.kind === "func")) return value;
|
||||
if (!primitive(value.type)) {
|
||||
lowerer.noLowering(
|
||||
@@ -64,7 +72,7 @@ export function lowerStringReplacement(
|
||||
return coerceStringSearchValue(lowerer, value, node, loc);
|
||||
};
|
||||
const search = argument(args[0], false);
|
||||
const replacement = argument(args[1], true);
|
||||
const replacement = argument(args[1], true, search.type.kind !== "dyn");
|
||||
if (search.type.kind === "dyn" || replacement.type.kind === "dyn") {
|
||||
// Evaluate all inputs once before choosing the search protocol. Regexes
|
||||
// retain their native pattern; plain values use the string-search helper.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
import { regexLiteralCaptures } from "../src/backend/llvm/regex-matchers.js";
|
||||
import { regexCaptureParticipation } from "../src/frontend/lowering/containers/replacer-params.js";
|
||||
|
||||
// The frontend binds a replace callback parameter as a string only for a
|
||||
// capture every match sets; the backend then calls the callback directly
|
||||
// only when its own parse agrees.
|
||||
describe("replace callback captures", () => {
|
||||
test("never claims a capture the backend's parse can leave unset", () => {
|
||||
const patterns = [
|
||||
"(\\w+)=(\\d)+",
|
||||
"(?<k>a)(b)?|(c)",
|
||||
"x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}",
|
||||
"((a)(b)?)+",
|
||||
"(a(b|c))d",
|
||||
"(?:a|(b))(c)",
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const frontend = regexCaptureParticipation(pattern, "");
|
||||
const backend = regexLiteralCaptures(pattern, "");
|
||||
expect(frontend?.count).toBe(backend?.count);
|
||||
frontend!.always.forEach((always, index) => {
|
||||
if (always) expect(backend!.always[index]).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => {
|
||||
await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function emit(source: string): Promise<string> {
|
||||
const outDir = await mkdtemp(join(tmpdir(), "scriptc-replacer-"));
|
||||
directories.push(outDir);
|
||||
const entry = join(outDir, "main.ts");
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(outDir, "program.ll");
|
||||
const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
return readFile(outPath, "utf8");
|
||||
}
|
||||
|
||||
/** The (arity, kinds) arguments of each direct replacer call, and the
|
||||
* number of calls through the boxed argument array. */
|
||||
function replacerCalls(llvm: string): { direct: string[]; boxed: number } {
|
||||
const lines = llvm.split("\n").filter((line) => line.includes("call "));
|
||||
return {
|
||||
direct: lines
|
||||
.filter((line) => line.includes("@scr_regex_replace_fn("))
|
||||
.map((line) => /i32 (-?\d+), i32 (-?\d+), i1/.exec(line)!.slice(1, 3).join(":")),
|
||||
boxed: lines.filter((line) => line.includes("@scr_regex_replace_callback(")).length,
|
||||
};
|
||||
}
|
||||
|
||||
describe("replace callback calls", () => {
|
||||
test("unannotated parameters take the kinds the replace passes", async () => {
|
||||
const llvm = await emit(`
|
||||
const vars: Record<string, string> = { a: "1" };
|
||||
const text = String(Date.now() > 0 ? "{a} {b}" : "");
|
||||
console.log(text.replace(/\\{(\\w+)\\}/g, (_m, key) => vars[key] ?? ""));
|
||||
console.log(text.replace(/\\{(\\w)/g, (_m, c) => c.toUpperCase()));
|
||||
console.log(text.replace(/(a)|(b)/g, (m, a, b) => \`\${m}\${a}\${b}\`));
|
||||
console.log(text.replace(/(a)(b)?/g, (m: string, a: string, b: string | undefined, at: number) => m + a + (b ?? "") + at));
|
||||
const half = (n: number): number => n / 2;
|
||||
console.log(text.replace(/b/g, (m, at) => String(half(at))));
|
||||
console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b + c + at + all + extra));
|
||||
`);
|
||||
expect(replacerCalls(llvm)).toEqual({
|
||||
// Typed parameters and result (0); a checked-dynamic result (bit 31);
|
||||
// two captures a match can leave unset (bits 1 and 2);
|
||||
// \`string | undefined\` at capture 2 (bit 8 + 2); an offset passed to a
|
||||
// number parameter keeps its checked-dynamic box (bit 1).
|
||||
direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2"],
|
||||
// Seven parameters exceed the direct calls' limit.
|
||||
boxed: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8118-replacer-params.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8345-replace-callback-untyped-params.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8346-replace-callback-untyped-params.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1529,49 +1529,101 @@ ScrStr *scr_regex_replace_callback(ScrStr *s, ScrRegex *re, ScrDyn *callback, bo
|
||||
return scr_jb_finish(&output);
|
||||
}
|
||||
|
||||
/* One call of a typed replacer (scr_regex_replace_fn's contract): string
|
||||
* arguments are passed +1 (the closure ABI's owned parameters), the offset
|
||||
* as a number, and the result is the closure's +1 string. */
|
||||
static ScrStr *scr_regex_call_replacer(ScrClosure *f, uint32_t arity, int ncap,
|
||||
ScrStr *const *caps, double offset, ScrStr *subject) {
|
||||
void *p[4] = { NULL, NULL, NULL, NULL };
|
||||
for (uint32_t i = 0; i < arity && i < 4; i++) {
|
||||
if ((int)i < ncap) {
|
||||
if (!caps[i]) scr_trap("scriptc: internal error: typed replacer capture is unset\n");
|
||||
p[i] = scr_str_retain(caps[i]);
|
||||
} else if ((int)i == ncap + 1) {
|
||||
p[i] = scr_str_retain(subject);
|
||||
}
|
||||
}
|
||||
/* The closure signature scr_regex_replace_fn calls: `arity` pointer
|
||||
* parameters, except a number at `number_at` (-1 when there is none). A
|
||||
* string and a checked-dynamic value are both pointers, and so are both
|
||||
* results. */
|
||||
#define SCR_REPLACER_CALL(...) return ((void *(*)(ScrClosure *, __VA_ARGS__))fn)
|
||||
static inline __attribute__((always_inline)) void *scr_regex_call_replacer(
|
||||
ScrClosure *f, uint32_t arity, int number_at, void *const *p, double offset) {
|
||||
void *fn = f->fn;
|
||||
typedef ScrStr *R;
|
||||
switch (arity) {
|
||||
case 0: return ((R (*)(ScrClosure *))fn)(f);
|
||||
case 1: return ((R (*)(ScrClosure *, void *))fn)(f, p[0]);
|
||||
case 2:
|
||||
if (ncap == 1) return ((R (*)(ScrClosure *, void *, double))fn)(f, p[0], offset);
|
||||
return ((R (*)(ScrClosure *, void *, void *))fn)(f, p[0], p[1]);
|
||||
case 3:
|
||||
if (ncap == 1) return ((R (*)(ScrClosure *, void *, double, void *))fn)(f, p[0], offset, p[2]);
|
||||
if (ncap == 2) return ((R (*)(ScrClosure *, void *, void *, double))fn)(f, p[0], p[1], offset);
|
||||
return ((R (*)(ScrClosure *, void *, void *, void *))fn)(f, p[0], p[1], p[2]);
|
||||
case 4:
|
||||
if (ncap == 2)
|
||||
return ((R (*)(ScrClosure *, void *, void *, double, void *))fn)(f, p[0], p[1], offset, p[3]);
|
||||
if (ncap == 3)
|
||||
return ((R (*)(ScrClosure *, void *, void *, void *, double))fn)(f, p[0], p[1], p[2], offset);
|
||||
if (ncap >= 4)
|
||||
return ((R (*)(ScrClosure *, void *, void *, void *, void *))fn)(f, p[0], p[1], p[2], p[3]);
|
||||
break;
|
||||
typedef void *P;
|
||||
typedef double D;
|
||||
switch (arity * 8 + (uint32_t)(number_at < 0 ? 7 : number_at)) {
|
||||
case 0 * 8 + 7: return ((void *(*)(ScrClosure *))fn)(f);
|
||||
case 1 * 8 + 7: SCR_REPLACER_CALL(P)(f, p[0]);
|
||||
case 2 * 8 + 7: SCR_REPLACER_CALL(P, P)(f, p[0], p[1]);
|
||||
case 2 * 8 + 1: SCR_REPLACER_CALL(P, D)(f, p[0], offset);
|
||||
case 3 * 8 + 7: SCR_REPLACER_CALL(P, P, P)(f, p[0], p[1], p[2]);
|
||||
case 3 * 8 + 1: SCR_REPLACER_CALL(P, D, P)(f, p[0], offset, p[2]);
|
||||
case 3 * 8 + 2: SCR_REPLACER_CALL(P, P, D)(f, p[0], p[1], offset);
|
||||
case 4 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P)(f, p[0], p[1], p[2], p[3]);
|
||||
case 4 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P)(f, p[0], offset, p[2], p[3]);
|
||||
case 4 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P)(f, p[0], p[1], offset, p[3]);
|
||||
case 4 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D)(f, p[0], p[1], p[2], offset);
|
||||
case 5 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P, P)(f, p[0], p[1], p[2], p[3], p[4]);
|
||||
case 5 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P, P)(f, p[0], offset, p[2], p[3], p[4]);
|
||||
case 5 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P, P)(f, p[0], p[1], offset, p[3], p[4]);
|
||||
case 5 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D, P)(f, p[0], p[1], p[2], offset, p[4]);
|
||||
case 5 * 8 + 4: SCR_REPLACER_CALL(P, P, P, P, D)(f, p[0], p[1], p[2], p[3], offset);
|
||||
case 6 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P, P, P)(f, p[0], p[1], p[2], p[3], p[4], p[5]);
|
||||
case 6 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P, P, P)(f, p[0], offset, p[2], p[3], p[4], p[5]);
|
||||
case 6 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P, P, P)(f, p[0], p[1], offset, p[3], p[4], p[5]);
|
||||
case 6 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D, P, P)(f, p[0], p[1], p[2], offset, p[4], p[5]);
|
||||
case 6 * 8 + 4: SCR_REPLACER_CALL(P, P, P, P, D, P)(f, p[0], p[1], p[2], p[3], offset, p[5]);
|
||||
case 6 * 8 + 5: SCR_REPLACER_CALL(P, P, P, P, P, D)(f, p[0], p[1], p[2], p[3], p[4], offset);
|
||||
default: break;
|
||||
}
|
||||
scr_trap("scriptc: internal error: typed replacer signature\n");
|
||||
}
|
||||
#undef SCR_REPLACER_CALL
|
||||
|
||||
/* Typed replacer argument k: a capture (NULL for an unset optional one)
|
||||
* or the subject; the offset is passed separately. The compiler admits a
|
||||
* typed parameter only at those positions. Inlined with each constant k. */
|
||||
static inline __attribute__((always_inline)) void scr_regex_typed_arg(
|
||||
void **p, uint32_t k, uint32_t kinds, int capture_count, ScrStr *const *caps, ScrStr *subject) {
|
||||
if ((int)k < capture_count) {
|
||||
if (!caps[k] && !(kinds & (SCR_REPLACER_OPTIONAL << k)))
|
||||
scr_trap("scriptc: internal error: typed replacer capture is unset\n");
|
||||
p[k] = caps[k] ? scr_str_retain(caps[k]) : NULL;
|
||||
} else if ((int)k == capture_count + 1) {
|
||||
p[k] = scr_str_retain(subject);
|
||||
}
|
||||
}
|
||||
|
||||
/* One match's replacer arguments when some parameter is checked-dynamic
|
||||
* (scr_regex_replace_fn's `kinds`): typed ones +1, the rest new boxes.
|
||||
* `boxed_subject` is the subject's box; `groups` the match's groups
|
||||
* object, or NULL when no parameter reads it. */
|
||||
__attribute__((noinline)) static void scr_regex_replacer_args(
|
||||
void **p, uint32_t arity, uint32_t kinds, int capture_count, ScrStr *const *caps, double offset,
|
||||
ScrStr *subject, ScrDyn *boxed_subject, ScrDyn *groups) {
|
||||
for (uint32_t k = 0; k < arity; k++) {
|
||||
int at = (int)k;
|
||||
if (kinds & (1u << k)) {
|
||||
if (at < capture_count)
|
||||
p[k] = caps[k] ? scr_dyn_new_str(caps[k]) : scr_dyn_retain(scr_dyn_undefined());
|
||||
else if (at == capture_count)
|
||||
p[k] = scr_dyn_new_num(offset);
|
||||
else if (at == capture_count + 1)
|
||||
p[k] = scr_dyn_retain(boxed_subject);
|
||||
else if (at == capture_count + 2 && groups)
|
||||
p[k] = scr_dyn_retain(groups);
|
||||
else
|
||||
p[k] = scr_dyn_retain(scr_dyn_undefined());
|
||||
} else if (at < capture_count) {
|
||||
if (!caps[k] && !(kinds & (SCR_REPLACER_OPTIONAL << k)))
|
||||
scr_trap("scriptc: internal error: typed replacer capture is unset\n");
|
||||
p[k] = caps[k] ? scr_str_retain(caps[k]) : NULL;
|
||||
} else if (at == capture_count + 1) {
|
||||
p[k] = scr_str_retain(subject);
|
||||
} else if (at != capture_count) {
|
||||
scr_trap("scriptc: internal error: typed replacer signature\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* scr_regex_replace_callback's algorithm for a typed replacer: the matches
|
||||
* are collected first, keeping only the captures the replacer reads, then
|
||||
* the replacer runs once per match in order. */
|
||||
ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, bool all) {
|
||||
* are collected first, keeping only the captures and groups objects the
|
||||
* replacer reads, then the replacer runs once per match in order. Typed
|
||||
* parameters receive their values +1 (the closure ABI's owned
|
||||
* parameters) and an unset optional capture NULL; checked-dynamic ones
|
||||
* receive new boxes. Inlined into one copy for kinds 0, where the boxed
|
||||
* handling folds away, and one for the other kinds. */
|
||||
static inline __attribute__((always_inline)) ScrStr *
|
||||
scr_regex_replace_fn_impl(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, uint32_t kinds,
|
||||
bool all) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
int flags = lre_get_flags(bc);
|
||||
bool global = (flags & LRE_FLAG_GLOBAL) != 0;
|
||||
@@ -1580,15 +1632,28 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
if (arity > SCR_REPLACER_MAX_ARITY) scr_trap("scriptc: internal error: typed replacer arity\n");
|
||||
bool sticky = (flags & LRE_FLAG_STICKY) != 0;
|
||||
bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
|
||||
int capture_count = lre_get_capture_count(bc);
|
||||
size_t keep = arity < (uint32_t)capture_count ? arity : (size_t)capture_count;
|
||||
/* Node passes the groups object after the subject when the pattern has
|
||||
* named groups; only a checked-dynamic parameter can read it, and no
|
||||
* typed parameter follows the subject. */
|
||||
bool past_subject = (uint32_t)capture_count + 2 < arity;
|
||||
if (past_subject && !(kinds & SCR_REPLACER_BOXED_PARAMS))
|
||||
scr_trap("scriptc: internal error: typed replacer signature\n");
|
||||
const char *groupnames = past_subject ? lre_get_groupnames(bc) : NULL;
|
||||
bool groups_read = groupnames != NULL;
|
||||
if (groups_read && !(kinds & (1u << (capture_count + 2))))
|
||||
scr_trap("scriptc: internal error: typed replacer groups parameter\n");
|
||||
int number_at = capture_count < (int)arity && !(kinds & (1u << capture_count)) ? capture_count : -1;
|
||||
ScrRegexInput input;
|
||||
scr_regex_input_init(&input, re, bc, s);
|
||||
int len = input.len;
|
||||
struct { int start, end; } *matches = NULL;
|
||||
ScrStr **caps = NULL;
|
||||
ScrDyn **groups = NULL;
|
||||
size_t count = 0, capacity = 0;
|
||||
if (global) re->last_index = 0;
|
||||
int position = sticky ? scr_regex_start(re, len) : 0;
|
||||
@@ -1610,6 +1675,11 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar
|
||||
if (!grown) scr_regex_oom();
|
||||
caps = grown;
|
||||
}
|
||||
if (groups_read) {
|
||||
grown = realloc(groups, next * sizeof(*groups));
|
||||
if (!grown) scr_regex_oom();
|
||||
groups = grown;
|
||||
}
|
||||
capacity = next;
|
||||
}
|
||||
for (size_t group = 0; group < keep; group++) {
|
||||
@@ -1618,6 +1688,18 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar
|
||||
cs && ce ? scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce))
|
||||
: NULL;
|
||||
}
|
||||
if (groups_read) {
|
||||
ScrDyn *object = scr_dyn_new_obj_null_proto();
|
||||
const char *name = groupnames;
|
||||
for (int group = 1; group < capture_count; group++) {
|
||||
size_t length = strlen(name);
|
||||
if (length && (capture[group * 2] || !scr_dyn_obj_get(object, name, length)))
|
||||
scr_dyn_obj_set(object, name, length,
|
||||
scr_regex_capture_value(&input, capture[group * 2], capture[group * 2 + 1]));
|
||||
name += length + LRE_GROUP_NAME_TRAILER_LEN;
|
||||
}
|
||||
groups[count] = object;
|
||||
}
|
||||
matches[count].start = start;
|
||||
matches[count].end = end;
|
||||
count++;
|
||||
@@ -1627,18 +1709,43 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar
|
||||
ScrJsonBuf output;
|
||||
scr_jb_init(&output);
|
||||
int next = 0;
|
||||
/* One box of the subject serves every match. */
|
||||
ScrDyn *subject = NULL;
|
||||
if (kinds & SCR_REPLACER_BOXED_PARAMS) subject = scr_dyn_new_str(s);
|
||||
scr_dyn_this_push_dyn(NULL);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
ScrStr *replacement = scr_regex_call_replacer(f, arity, capture_count, caps + i * keep,
|
||||
(double)matches[i].start, s);
|
||||
if (scr_exc_pending()) break;
|
||||
void *p[SCR_REPLACER_MAX_ARITY] = { NULL };
|
||||
double offset = (double)matches[i].start;
|
||||
ScrStr *const *match_caps = caps + i * keep;
|
||||
if (kinds & SCR_REPLACER_BOXED_PARAMS) {
|
||||
scr_regex_replacer_args(p, arity, kinds, capture_count, match_caps, offset, s, subject,
|
||||
groups_read ? groups[i] : NULL);
|
||||
} else {
|
||||
for (uint32_t k = 0; k < SCR_REPLACER_MAX_ARITY; k++)
|
||||
if (k < arity) scr_regex_typed_arg(p, k, kinds, capture_count, match_caps, s);
|
||||
}
|
||||
void *result = scr_regex_call_replacer(f, arity, number_at, p, offset);
|
||||
ScrStr *replacement = (ScrStr *)result;
|
||||
if (kinds & SCR_REPLACER_BOXED_RESULT) {
|
||||
replacement = scr_exc_pending() ? NULL : scr_dyn_string_coerce_js((ScrDyn *)result);
|
||||
scr_dyn_release((ScrDyn *)result);
|
||||
}
|
||||
if (scr_exc_pending()) {
|
||||
scr_str_release(replacement);
|
||||
break;
|
||||
}
|
||||
scr_regex_put_span(&output, &input, next, matches[i].start);
|
||||
scr_jb_put_str(&output, replacement);
|
||||
scr_str_release(replacement);
|
||||
next = matches[i].end;
|
||||
}
|
||||
scr_dyn_this_pop();
|
||||
if (subject) scr_dyn_release(subject);
|
||||
for (size_t i = 0; i < count * keep; i++) scr_str_release(caps[i]);
|
||||
if (groups_read) {
|
||||
for (size_t i = 0; i < count; i++) scr_dyn_release(groups[i]);
|
||||
free(groups);
|
||||
}
|
||||
free(caps);
|
||||
free(matches);
|
||||
if (scr_exc_pending()) {
|
||||
@@ -1651,6 +1758,24 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar
|
||||
return scr_jb_finish(&output);
|
||||
}
|
||||
|
||||
__attribute__((noinline)) static ScrStr *scr_regex_replace_typed(ScrStr *s, ScrRegex *re,
|
||||
ScrClosure *f, uint32_t arity,
|
||||
bool all) {
|
||||
return scr_regex_replace_fn_impl(s, re, f, arity, 0, all);
|
||||
}
|
||||
|
||||
__attribute__((noinline)) static ScrStr *scr_regex_replace_kinds(ScrStr *s, ScrRegex *re,
|
||||
ScrClosure *f, uint32_t arity,
|
||||
uint32_t kinds, bool all) {
|
||||
return scr_regex_replace_fn_impl(s, re, f, arity, kinds, all);
|
||||
}
|
||||
|
||||
ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity,
|
||||
uint32_t kinds, bool all) {
|
||||
if (kinds == 0) return scr_regex_replace_typed(s, re, f, arity, all);
|
||||
return scr_regex_replace_kinds(s, re, f, arity, kinds, all);
|
||||
}
|
||||
|
||||
/* The pinned Node ANSI matcher derives from ansi-regex, copyright (c)
|
||||
* Sindre Sorhus <sindresorhus@gmail.com>, used under the MIT license:
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
|
||||
@@ -80,16 +80,26 @@ 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);
|
||||
/* replace/replaceAll with a typed replacer closure of `arity` (at most 4)
|
||||
* parameters returning a string. Parameter i receives what Node passes at
|
||||
* position i: the match and captures (strings) below the capture count n,
|
||||
* the match offset (a number) at n, the subject (a string) at n + 1. The
|
||||
* compiler admits a closure only when every position it declares is one of
|
||||
* those with the declared kind, and every capture it reads is set in every
|
||||
* match (scr_regex_replace_callback handles the rest). Borrows all. */
|
||||
/* 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
|
||||
* match offset at n, the subject at n + 1, the groups object at n + 2 when
|
||||
* the pattern has named groups, and undefined after that. Bit i of
|
||||
* `kinds` makes parameter i a checked-dynamic value, and bit
|
||||
* SCR_REPLACER_OPTIONAL << i a capture held as a string or NULL for
|
||||
* undefined; otherwise it is a string, or a number at n.
|
||||
* SCR_REPLACER_BOXED_RESULT makes the result a checked-dynamic value,
|
||||
* converted with ToString. The compiler admits a closure only when every
|
||||
* typed position it declares is one of those with the declared kind, and
|
||||
* every capture a string parameter reads is set in every match
|
||||
* (scr_regex_replace_callback handles the rest). Borrows all. */
|
||||
#define SCR_REPLACER_MAX_ARITY 6
|
||||
#define SCR_REPLACER_BOXED_PARAMS 0xffu
|
||||
#define SCR_REPLACER_OPTIONAL (1u << 8)
|
||||
#define SCR_REPLACER_BOXED_RESULT (1u << 31)
|
||||
struct ScrClosure;
|
||||
ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, struct ScrClosure *f, uint32_t arity,
|
||||
bool all);
|
||||
uint32_t kinds, bool all);
|
||||
/* replaceAll: throws Node's TypeError when /g is missing (may-throw). */
|
||||
ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep);
|
||||
/* split: captured substrings are interleaved with the split pieces;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Inline replace callbacks whose parameters after the match are left
|
||||
// untyped: captures every match sets, captures that can be unset (in an
|
||||
// alternation, an optional or zero-repeat group, a lookahead), the offset,
|
||||
// the subject and the groups object, reassigned parameters, named groups,
|
||||
// patterns with classes, escapes and lookbehind, function expressions and
|
||||
// replaceAll.
|
||||
const lines = ["id7,tag12;tag3", "x9 y10", "", "naïve 😀 42", "a-b-c"];
|
||||
for (const line of lines) {
|
||||
console.log(line.replace(/tag(\d+)/g, (_m, d) => String(Number(d) + 1)));
|
||||
console.log(line.replace(/(\w)(\d+)/g, (m, letter, digits, offset, subject) =>
|
||||
`[${letter}:${digits.length}@${offset}/${subject.length}]`));
|
||||
console.log(line.replace(/(a)|(\d)/g, (m, a, digit) => `<${typeof a},${typeof digit}>`));
|
||||
console.log(line.replace(/(x)?(\d)/g, (m, x, d) => `${x === undefined ? "-" : x}${d}`));
|
||||
console.log(line.replace(/(?:(t)a)*g/g, (m, t, offset) => `{${t}|${offset}}`));
|
||||
console.log(line.replace(/(?=(\d))\d/g, (m, ahead, offset) => `${ahead}${offset}`));
|
||||
console.log(line.replace(/(?<n>\d)(\d)?/g, (m, n, rest, offset, subject, groups) =>
|
||||
`${n}${rest ?? "_"}${offset}${typeof subject}${JSON.stringify(groups)}`));
|
||||
console.log(line.replace(/([\(\)\[\]]|[a-c])(-)/g, (m, c, dash) => c.toUpperCase() + dash.length));
|
||||
console.log(line.replace(/(?<=-)(\w)/g, (m, c, offset) => c + offset.toFixed(1)));
|
||||
console.log(line.replaceAll(/(\d)/g, function (m, d, offset) {
|
||||
return `${d * 2}:${offset + 1}`;
|
||||
}));
|
||||
console.log(line.replace(/(\d)/g, (m, d) => {
|
||||
d = d.length > 0 ? 5 : 6;
|
||||
return String(d + 1);
|
||||
}));
|
||||
console.log(line.replace(/(\d)\1?/g, (m, d, offset, subject) => `${d}${offset}${subject === line}`));
|
||||
console.log(line.replace(/(.)/u, (m, c, offset, subject, extra) => `${c.codePointAt(0)}${offset}${extra}`));
|
||||
console.log(line.replace(/(\d){0,2}x/g, (m, d) => `${d}`));
|
||||
console.log(line.replace(/(\w)(\w)/g, (m, a, b, offset) =>
|
||||
[a.toUpperCase(), b.toLowerCase(), a.trim(), a.charCodeAt(0), b.codePointAt(0), a.slice(0, 1),
|
||||
b.substring(0), a.repeat(2), b.padStart(3, "."), a.padEnd(2, "!"), b.includes("1"),
|
||||
a.startsWith("t"), b.endsWith("g"), a.indexOf("a"), a.length, parseInt(b, 36),
|
||||
parseFloat(b), Number(b), String(offset), offset - 1, offset * 2, offset % 3, offset < 4,
|
||||
a === b, a !== "x", a + offset, offset + 1, `${offset}`].join(",")));
|
||||
console.log(line.replace(/(\d+)/g, (m, d) => (d)));
|
||||
console.log(line.replace(/(\d+)/g, function (m, d) { return d + d; }));
|
||||
}
|
||||
let count = 0;
|
||||
console.log("tag1 tag2".replace(/tag(\d)/g, (m, d) => {
|
||||
count += Number(d);
|
||||
return d.repeat(2);
|
||||
}), count);
|
||||
@@ -0,0 +1,85 @@
|
||||
// String replace/replaceAll with inline callbacks whose parameters have no
|
||||
// annotations, so the lib types every parameter after the match `any`.
|
||||
// Each such parameter holds what Node passes at its position: the captures,
|
||||
// the offset, the subject, the groups object and then undefined. Captures a
|
||||
// match can leave unset hold undefined, and a result that is not a string
|
||||
// converts with ToString.
|
||||
const vars: Record<string, string> = { name: "world", place: "earth" };
|
||||
const template = "Hello {name}, welcome to {place}! {missing} {name}.";
|
||||
console.log(template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
|
||||
console.log(template.replace(/\{(\w+)\}/, (_m, key) => key.toUpperCase()));
|
||||
console.log("some_snake_case_name".replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
|
||||
console.log("2024-01-15 and 1999-07-04".replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, mo, d) => `${d}/${mo}/${y}`));
|
||||
console.log("a1b22".replace(/\d+/g, (m, offset, subject) => `[${m}@${offset}/${subject.length}]`));
|
||||
console.log("x1y2".replace(/(\d)/g, (m, digit, offset, subject) => `${m}${digit}${offset}${subject}`));
|
||||
console.log("k=v".replace(/(\w)=(\w)/, function (m, k, v, at, all) {
|
||||
return `${v}=${k}:${at}:${all}:${m}`;
|
||||
}));
|
||||
|
||||
console.log("k=v".replace(/(\w)=(\w)/, function (this: void, m, k, v, at) {
|
||||
return `${typeof this}:${v}=${k}:${at}:${m}`;
|
||||
}));
|
||||
|
||||
// A parameter the body assigns keeps `any`.
|
||||
console.log("a1".replace(/(\d)/, (_m, d) => {
|
||||
d = Number(d) + 1;
|
||||
return String(d * 2);
|
||||
}));
|
||||
|
||||
// Captures that a match can leave unset.
|
||||
console.log("ab".replace(/(a)|(b)/g, (m, a, b) => `${m}:${a}:${b};`));
|
||||
console.log("b xb".replace(/(x)?b/g, (m, x, at) => `${m}:${x}:${at}`));
|
||||
console.log("ab".replace(/((a)|b)/g, (m, outer, inner) => `${outer}:${inner};`));
|
||||
console.log("ab".replace(/(?:(a)|b)/g, (m, a, at) => `${m}:${a}:${at};`));
|
||||
console.log("aab".replace(/(a){0,2}b/, (m, a, at) => `${m}:${a}:${at}`));
|
||||
console.log("ab".replace(/(?=(a))a|b/g, (m, look, at) => `${m}:${look}:${at};`));
|
||||
console.log("ab".replace(/(?!(x))a/g, (m, x, at) => `${m}:${x}:${at};`));
|
||||
console.log("ab".replace(/(a)*b/, (m, a, at) => `${m}:${a}:${at}`));
|
||||
console.log("a(b)".replace(/\((b)\)/, (m, b, at) => `${m}:${b}:${at}`));
|
||||
console.log("a|b".replace(/[|(](b)?/, (m, b, at) => `${m}:${b}:${at}`));
|
||||
|
||||
// Annotated `string | undefined` captures hold undefined when unset.
|
||||
console.log("2024-01-15 1999-07".replace(/(\d{4})-(\d{2})(-(\d{2}))?/g, (_m, y: string, mo: string, _x: string | undefined, d: string | undefined) => `${d ?? "??"}/${mo}/${y}`));
|
||||
console.log("ab".replace(/(a)|(b)/g, (m: string, a: string | undefined, b: string | undefined, at: number) => `${m}:${a ?? "-"}:${b ?? "-"}:${at};`));
|
||||
console.log("abc".replace(/(a)(b)(c)/, (m: string, a: string, b: string, c: string, at: number, all: string) => `${c}${b}${a}${m}${at}${all}`));
|
||||
console.log("abcd".replace(/(a)(b)(c)(d)/, (m, a, b, c, d, at, all) => `${d}${c}${b}${a}${m}${at}${all}`));
|
||||
|
||||
// Named groups pass the groups object after the subject; later parameters
|
||||
// are undefined.
|
||||
console.log("2024-01".replace(/(?<y>\d{4})-(?<m>\d{2})/, (m, y, mo, at, all, groups) => `${groups.m}.${groups.y}:${y}${mo}:${at}:${all}`));
|
||||
console.log("ab".replace(/(?<a>a)|(?<b>b)/g, (m, a, b, at, all, groups) => `${m}:${JSON.stringify(groups)};`));
|
||||
console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`));
|
||||
console.log("ab".replace(/(a)(b)/, (m, a, b, at, all, extra, more) => `${m}${a}${b}${at}${all}${extra}${more}`));
|
||||
|
||||
// Results that are not strings convert with ToString.
|
||||
const counts: Record<string, any> = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } };
|
||||
console.log("a b c d e".replace(/\w/g, (m) => counts[m]));
|
||||
console.log("abc".replace(/(\w)/g, (m, c) => counts[c]));
|
||||
|
||||
// String search patterns pass (match, offset, subject).
|
||||
console.log("one two one".replaceAll("one", (m, at, subject) => `${m.length}@${at}/${subject.length}`));
|
||||
console.log("one two one".replace("two", (m, at, subject, extra) => `${m}${at}${subject.length}${extra}`));
|
||||
|
||||
// A throw stops the replacement.
|
||||
try {
|
||||
"a1b2c3".replace(/(\d)/g, (_m, d) => {
|
||||
if (d === "2") throw new Error("stop at " + d);
|
||||
return "#";
|
||||
});
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
try {
|
||||
"a1".replace(/(\d)/g, (_m, d) => counts.e.missing.field + d);
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError);
|
||||
}
|
||||
try {
|
||||
"ab".replace(/(a)|b/g, (m, a) => a.toUpperCase());
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError, (error as Error).message);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 2000; i++) total += template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? String(i)).length;
|
||||
console.log(total);
|
||||
@@ -0,0 +1,32 @@
|
||||
// JavaScript string replace/replaceAll with inline callbacks. Each
|
||||
// parameter holds what Node passes at its position: the captures, the
|
||||
// offset, the subject, the groups object and then undefined. Captures a
|
||||
// match can leave unset hold undefined, and a result that is not a string
|
||||
// converts with ToString.
|
||||
const vars = { name: "world", place: "earth" };
|
||||
const template = "Hello {name}, welcome to {place}! {missing} {name}.";
|
||||
console.log(template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
|
||||
console.log("some_snake_case_name".replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
|
||||
console.log("2024-01-15 and 1999-07-04".replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, mo, d) => `${d}/${mo}/${y}`));
|
||||
console.log("a1b22".replace(/\d+/g, (m, offset, subject) => `[${m}@${offset}/${subject.length}]`));
|
||||
console.log("k=v".replace(/(\w)=(\w)/, function (m, k, v, at, all) {
|
||||
return `${this === undefined}:${v}=${k}:${at}:${all}:${m}`;
|
||||
}));
|
||||
console.log("ab".replace(/(a)|(b)/g, (m, a, b) => `${m}:${a}:${b};`));
|
||||
console.log("b xb".replace(/(x)?b/g, (m, x, at) => `${m}:${x}:${at}`));
|
||||
console.log("2024-01".replace(/(?<y>\d{4})-(?<m>\d{2})/, (m, y, mo, at, all, groups) => `${groups.m}.${groups.y}:${y}${mo}:${at}:${all}`));
|
||||
console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`));
|
||||
const counts = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } };
|
||||
console.log("a b c d e".replace(/\w/g, (m) => counts[m]));
|
||||
console.log("1-2".replace(/(\d)/g, (_m, d) => Number(d) * 10));
|
||||
console.log("one two one".replaceAll("one", (m, at, subject) => `${m.length}@${at}/${subject.length}`));
|
||||
let seen = "";
|
||||
console.log("xyz".replace(/(y)/, (m, y, at, subject) => {
|
||||
seen = typeof m + typeof y + typeof at + typeof subject;
|
||||
return y + at;
|
||||
}), seen);
|
||||
try {
|
||||
"ab".replace(/(a)|b/g, (m, a) => a.toUpperCase());
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError, error.message);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3670, "san": 4410 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3630, "san": 4420 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2880, "san": 5080 }
|
||||
Reference in New Issue
Block a user