mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Reuse regex literal sites and scan plain patterns directly (#918)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted } from "../../ir/ir.js";
|
||||
import { isRefCounted, type IrExpr } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -484,6 +484,26 @@ export function emitOperatorExpr(
|
||||
}
|
||||
}
|
||||
|
||||
/** The immortal template of a regex literal: one static per (pattern,
|
||||
* flags) pair. Pattern/flags strings intern NOW (the literal table is still
|
||||
* open — the runtime ABI's regex-literal discipline). */
|
||||
export function regexLiteralTemplate(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "regexLit" },
|
||||
): string {
|
||||
const key = `${e.flags}/${e.pattern}`;
|
||||
let re = host.regexInstances.get(key);
|
||||
if (!re) {
|
||||
re = {
|
||||
sym: `sc_re_${host.regexInstances.size}`,
|
||||
src: host.internLiteral(e.pattern),
|
||||
fl: host.internLiteral(e.flags),
|
||||
};
|
||||
host.regexInstances.set(key, re);
|
||||
}
|
||||
return `@${re.sym}`;
|
||||
}
|
||||
|
||||
export function emitStringExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -666,23 +686,12 @@ export function emitStringExpr(
|
||||
case "strIntrinsic":
|
||||
return host.emitStrIntrinsic(e);
|
||||
case "regexLit": {
|
||||
// One immortal static per (pattern, flags) pair; the +1 retain is
|
||||
// a no-op on immortals but keeps the owned-temps discipline
|
||||
// uniform. Pattern/flags strings intern NOW (the literal table is
|
||||
// still open — the runtime ABI’s regex-literal discipline).
|
||||
const key = `${e.flags}/${e.pattern}`;
|
||||
let re = host.regexInstances.get(key);
|
||||
if (!re) {
|
||||
re = {
|
||||
sym: `sc_re_${host.regexInstances.size}`,
|
||||
src: host.internLiteral(e.pattern),
|
||||
fl: host.internLiteral(e.flags),
|
||||
};
|
||||
host.regexInstances.set(key, re);
|
||||
}
|
||||
// Each evaluation is a fresh RegExp over the site's shared template
|
||||
// (compiled bytecode and matcher); the +1 result is owned.
|
||||
const template = regexLiteralTemplate(host, e);
|
||||
host.declare("declare ptr @scr_regex_literal(ptr)");
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = call ptr @scr_regex_literal(ptr @${re.sym})`);
|
||||
B.line(`${value} = call ptr @scr_regex_literal(ptr ${template})`);
|
||||
return host.own({ name: value, type: e.type });
|
||||
}
|
||||
case "templateStrings": {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { type IrExpr, type IrType, isRefCounted, type SrcLoc, typeEquals } from
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, llvmCommentText } from "./common.js";
|
||||
import { emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { regexLiteralTemplate } from "./expr-primitives.js";
|
||||
import { preservesRegexInputs } from "./reference-effects.js";
|
||||
|
||||
/** Widen one tagged read. The source is borrowed unless the overflow map
|
||||
@@ -128,6 +129,24 @@ function rebuildRetype(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The matching operations whose args[0] is the regex they consume. */
|
||||
function directRegexOperandMethod(method: string): boolean {
|
||||
switch (method) {
|
||||
case "exec":
|
||||
case "execGroups":
|
||||
case "match":
|
||||
case "search":
|
||||
case "matchAll":
|
||||
case "matchAllInto":
|
||||
case "replace":
|
||||
case "replaceAll":
|
||||
case "split":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function emitRegexIntrinsic(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "regexIntrinsic" },
|
||||
@@ -135,11 +154,38 @@ export function emitRegexIntrinsic(
|
||||
const B = host.B;
|
||||
const method = e.method;
|
||||
const inputs = [e.receiver, ...e.args];
|
||||
const values = preservesRegexInputs(method)
|
||||
? emitBorrowedInputs(host, inputs)
|
||||
: method === "matchAllInto"
|
||||
? inputs.map((a) => emitBorrowedInput(host, a))
|
||||
: inputs.map((a) => host.emitExpr(a));
|
||||
// A regex literal consumed directly by a matching operation cannot be
|
||||
// observed afterwards: nothing else holds the object, so its identity is
|
||||
// unobservable and its lastIndex is never read again. Such a site runs on
|
||||
// the literal's immortal template instead of a fresh copy, resetting a
|
||||
// global or sticky template's lastIndex to the fresh value right before
|
||||
// the call (after every other operand, which may evaluate the same site).
|
||||
const regexAt = method === "test" ? 0 : directRegexOperandMethod(method) ? 1 : -1;
|
||||
const literal = regexAt >= 0 ? inputs[regexAt]! : null;
|
||||
const template =
|
||||
literal !== null && literal.kind === "regexLit" ? regexLiteralTemplate(host, literal) : null;
|
||||
let values: LlValue[];
|
||||
if (template !== null) {
|
||||
const borrowed = borrowableInputs(host, inputs);
|
||||
values = inputs.map((a, index) =>
|
||||
index === regexAt
|
||||
? { name: template, type: a.type }
|
||||
: borrowed[index]
|
||||
? host.emitReadReceiver(a)
|
||||
: host.emitExpr(a),
|
||||
);
|
||||
if (/[gy]/.test((literal as IrExpr & { kind: "regexLit" }).flags)) {
|
||||
const field = B.tmp();
|
||||
B.line(`${field} = getelementptr inbounds %ScrRegex, ptr ${template}, i32 0, i32 4`);
|
||||
B.line(`store double 0.0, ptr ${field}`);
|
||||
}
|
||||
} else {
|
||||
values = preservesRegexInputs(method)
|
||||
? emitBorrowedInputs(host, inputs)
|
||||
: method === "matchAllInto"
|
||||
? inputs.map((a) => emitBorrowedInput(host, a))
|
||||
: inputs.map((a) => host.emitExpr(a));
|
||||
}
|
||||
const r = values[0]!;
|
||||
const args = values.slice(1);
|
||||
const fallible = (sym: string, argText: string): LlValue => {
|
||||
@@ -190,7 +236,8 @@ export function emitRegexIntrinsic(
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "match":
|
||||
case "exec": {
|
||||
case "exec":
|
||||
case "execGroups": {
|
||||
// +1 string[] or NULL from the runtime; the `string[] | null`
|
||||
// union wraps type-directedly, the envGet convention.
|
||||
if (e.type.kind !== "union")
|
||||
@@ -200,10 +247,19 @@ export function emitRegexIntrinsic(
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
if (arrTag < 0 || nullTag < 0 || !def)
|
||||
throw new InternalCompilerError("llvm emitter bug: match union lacks its arms");
|
||||
const symbol = e.method === "exec" ? "scr_regex_exec" : "scr_regex_match";
|
||||
host.declare(`declare ptr @${symbol}(ptr, ptr)`);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @${symbol}(ptr ${r.name}, ptr ${args[0]!.name})`);
|
||||
if (e.method === "execGroups") {
|
||||
// args[1] is the constant mask of the captures the result's
|
||||
// readers use (see the IR method's contract).
|
||||
host.declare(`declare ptr @scr_regex_exec_groups(ptr, ptr, double)`);
|
||||
B.line(
|
||||
`${raw} = call ptr @scr_regex_exec_groups(ptr ${r.name}, ptr ${args[0]!.name}, double ${args[1]!.name})`,
|
||||
);
|
||||
} else {
|
||||
const symbol = e.method === "exec" ? "scr_regex_exec" : "scr_regex_match";
|
||||
host.declare(`declare ptr @${symbol}(ptr, ptr)`);
|
||||
B.line(`${raw} = call ptr @${symbol}(ptr ${r.name}, ptr ${args[0]!.name})`);
|
||||
}
|
||||
return host.wrapNullable(raw, raw, def.arms[arrTag]!, arrTag, e.type, nullTag);
|
||||
}
|
||||
case "search": {
|
||||
|
||||
@@ -77,6 +77,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.parseInt": "scr_parse_int",
|
||||
"num.parseFloat": "scr_parse_float",
|
||||
"str.localeCompare": "scr_str_locale_compare",
|
||||
"str.replaceLiteral": "scr_str_replace_literal",
|
||||
"num.fromString": "scr_string_to_number",
|
||||
"math.round": "scr_math_round",
|
||||
"math.sin": "sin",
|
||||
|
||||
@@ -23,6 +23,7 @@ export function preservesRegexInputs(method: string): boolean {
|
||||
switch (method) {
|
||||
case "test":
|
||||
case "exec":
|
||||
case "execGroups":
|
||||
case "match":
|
||||
case "search":
|
||||
case "matchAll":
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { identifiersNamed } from "../identifier-uses.js";
|
||||
|
||||
/** The captures an `exec` result is read through, as a bit mask (bit k for
|
||||
* capture k), when `const m = re.exec(s)` is observed only by null tests
|
||||
* (`m === null`, `m != null`, ...) and constant-index element reads
|
||||
* (`m[3]`, `m[3]!`). No other use can tell an unread capture, or the
|
||||
* result's `index`/`input` properties, from a missing one, so the runtime
|
||||
* leaves them out. Null when any use has another shape or the binding is
|
||||
* exported, which keeps the complete result. */
|
||||
export function execResultCaptureMask(lowerer: Lowerer, call: ts.CallExpression): number | null {
|
||||
const decl = call.parent;
|
||||
if (
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.initializer !== call ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.type !== undefined ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
const statement = decl.parent.parent;
|
||||
if (
|
||||
ts.isVariableStatement(statement) &&
|
||||
statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
||||
)
|
||||
return null;
|
||||
const symbol = lowerer.resolveValueSymbol(decl.name);
|
||||
if (!symbol) return null;
|
||||
const read = new Set<number>();
|
||||
for (const use of identifiersNamed(lowerer, decl.getSourceFile(), decl.name.text)) {
|
||||
if (use === decl.name || lowerer.resolveValueSymbol(use) !== symbol) continue;
|
||||
const ref = outerValue(use);
|
||||
const parent = ref.parent;
|
||||
if (ts.isBinaryExpression(parent) && EQUALITY.has(parent.operatorToken.kind)) {
|
||||
const other = parent.left === ref ? parent.right : parent.left;
|
||||
if (other.kind === ts.SyntaxKind.NullKeyword) continue;
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
ts.isElementAccessExpression(parent) &&
|
||||
parent.expression === ref &&
|
||||
parent.questionDotToken === undefined &&
|
||||
ts.isNumericLiteral(parent.argumentExpression) &&
|
||||
isValueRead(parent)
|
||||
) {
|
||||
const index = Number(parent.argumentExpression.text);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= 53) return null;
|
||||
read.add(index);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let mask = 0;
|
||||
for (const index of read) mask += 2 ** index;
|
||||
return mask;
|
||||
}
|
||||
|
||||
const EQUALITY = new Set<ts.SyntaxKind>([
|
||||
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
||||
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
||||
ts.SyntaxKind.EqualsEqualsToken,
|
||||
ts.SyntaxKind.ExclamationEqualsToken,
|
||||
]);
|
||||
|
||||
function outerValue(node: ts.Node): ts.Node {
|
||||
let ref = node;
|
||||
while (ts.isParenthesizedExpression(ref.parent) || ts.isNonNullExpression(ref.parent))
|
||||
ref = ref.parent;
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** An element read whose value is only consumed: not a write, update,
|
||||
* delete or destructuring target. */
|
||||
function isValueRead(access: ts.ElementAccessExpression): boolean {
|
||||
const ref = outerValue(access);
|
||||
const parent = ref.parent;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === ref &&
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return false;
|
||||
return !(
|
||||
(ts.isPrefixUnaryExpression(parent) &&
|
||||
(parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
parent.operator === ts.SyntaxKind.MinusMinusToken)) ||
|
||||
ts.isPostfixUnaryExpression(parent) ||
|
||||
ts.isDeleteExpression(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isObjectLiteralElementLike(parent) ||
|
||||
ts.isSpreadElement(parent) ||
|
||||
((ts.isForOfStatement(parent) || ts.isForInStatement(parent)) && parent.initializer === ref)
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import type { Lowerer } from "../lowerer.js";
|
||||
import { own } from "../lowerer.js";
|
||||
import { isRequireMainFilename } from "../expressions/optional-chains.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 {
|
||||
coerceStringSearchValue,
|
||||
@@ -767,17 +768,23 @@ export function lowerRegexMethodCall(
|
||||
kind: "union",
|
||||
unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]),
|
||||
};
|
||||
// A result read only through null tests and constant indices skips
|
||||
// the captures and properties no reader uses.
|
||||
const mask = execResultCaptureMask(lowerer, call);
|
||||
const execCall = (regex: IrExpr): IrExpr => ({
|
||||
kind: "regexIntrinsic",
|
||||
method: mask === null ? "exec" : "execGroups",
|
||||
receiver: subject,
|
||||
args: mask === null ? [regex] : [regex, numLit(mask, loc)],
|
||||
type: resultT,
|
||||
loc,
|
||||
});
|
||||
// A literal receiver has no evaluation to order against the subject.
|
||||
if (re.kind === "regexLit") return execCall(re);
|
||||
// The shared match intrinsic takes the string first; preserve exec's
|
||||
// receiver-before-subject evaluation order before swapping operands.
|
||||
const saved = lowerer.declareHiddenLocal("%execReceiver", re.type);
|
||||
const result: IrExpr = {
|
||||
kind: "regexIntrinsic",
|
||||
method: "exec",
|
||||
receiver: subject,
|
||||
args: [varRef(saved.id, re.type, loc)],
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
const result = execCall(varRef(saved.id, re.type, loc));
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: saved.id, init: re, loc }],
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
typeEquals,
|
||||
typeKey,
|
||||
type IrExpr,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import { numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from "../optional-arguments.js";
|
||||
|
||||
/** String search does not interpret regex syntax or capture substitutions.
|
||||
* The helper receives evaluated arguments and owns its chunks until join.
|
||||
* A string replacement expands in one runtime pass; a callback runs through
|
||||
* a helper that receives evaluated arguments and owns its chunks until join.
|
||||
* Callbacks see the original immutable subject and UTF-16 match offset. */
|
||||
export function lowerStringReplacement(
|
||||
lowerer: Lowerer,
|
||||
@@ -127,13 +128,30 @@ function lowerStringReplacementValues(
|
||||
replacement: IrExpr,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const callback = replacement.type.kind === "func" ? replacement.type : null;
|
||||
if (replacement.type.kind === "string") {
|
||||
// A replacement template expands in one runtime pass over the subject.
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "str.replaceLiteral",
|
||||
args: [
|
||||
receiver,
|
||||
search,
|
||||
replacement,
|
||||
{ kind: "boolLit", value: method === "replaceAll", type: BOOL, loc },
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const callback = replacement.type;
|
||||
if (callback.kind !== "func") {
|
||||
throw new InternalCompilerError(`string .${method} replacement of type ${callback.kind}`);
|
||||
}
|
||||
const full = [STRING, F64, STRING];
|
||||
if (
|
||||
callback &&
|
||||
(callback.ret.kind !== "string" ||
|
||||
callback.params.length > full.length ||
|
||||
!callback.params.every((type, index) => typeEquals(type, full[index]!)))
|
||||
callback.ret.kind !== "string" ||
|
||||
callback.params.length > full.length ||
|
||||
!callback.params.every((type, index) => typeEquals(type, full[index]!))
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`string .${method} callback signature`,
|
||||
@@ -156,21 +174,13 @@ function lowerStringReplacementValues(
|
||||
const chunksType = arrayOf(STRING);
|
||||
const chunks = varRef("chunks.0", chunksType, loc);
|
||||
const builder = stringBuilder(loc, chunks);
|
||||
const value: IrExpr = callback
|
||||
? {
|
||||
kind: "callValue",
|
||||
callee: replace,
|
||||
args: [needle, match, subject].slice(0, callback.params.length),
|
||||
type: STRING,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "call",
|
||||
callee: replacementTemplateHelper(lowerer, loc),
|
||||
args: [subject, needle, replace, match],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const value: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: replace,
|
||||
args: [needle, match, subject].slice(0, callback.params.length),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const stop: IrStmt = { kind: "break", loc };
|
||||
const body: IrStmt[] = [
|
||||
builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }),
|
||||
@@ -251,121 +261,6 @@ function lowerStringReplacementValues(
|
||||
return { kind: "call", callee: helper, args: [receiver, search, replacement], type: STRING, loc };
|
||||
}
|
||||
|
||||
/** GetSubstitution with no captures: $$, $&, $`, and $' expand. Numeric
|
||||
* and named capture spellings remain literal. Scan dollar signs rather
|
||||
* than individual subject characters so ordinary Unicode text is intact. */
|
||||
function replacementTemplateHelper(lowerer: Lowerer, loc: SrcLoc): string {
|
||||
const key = "str.replacementTemplate";
|
||||
const previous = lowerer.valueHelpers.get(key);
|
||||
if (previous) return previous;
|
||||
const name = `%str.template.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const subject = varRef("s.0", STRING, loc);
|
||||
const needle = varRef("search.0", STRING, loc);
|
||||
const template = varRef("template.0", STRING, loc);
|
||||
const position = varRef("position.0", F64, loc);
|
||||
const cursor = varRef("cursor.0", F64, loc);
|
||||
const dollar = varRef("dollar.0", F64, loc);
|
||||
const next = varRef("next.0", STRING, loc);
|
||||
const chunksType = arrayOf(STRING);
|
||||
const chunks = varRef("chunks.0", chunksType, loc);
|
||||
const builder = stringBuilder(loc, chunks);
|
||||
const expansions: [string, IrExpr][] = [
|
||||
["$", strLit("$", loc)],
|
||||
["&", needle],
|
||||
["`", builder.slice(subject, numLit(0, loc), position)],
|
||||
["'", builder.slice(subject, builder.add(position, builder.length(needle)), undefined)],
|
||||
];
|
||||
let selection: IrStmt[] = [builder.append(strLit("$", loc))];
|
||||
for (let index = expansions.length - 1; index >= 0; index--) {
|
||||
const [token, expansion] = expansions[index]!;
|
||||
selection = [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: next,
|
||||
right: strLit(token, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
builder.append(expansion),
|
||||
builder.assign("cursor.0", builder.add(cursor, numLit(1, loc))),
|
||||
],
|
||||
else_: selection,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
const params = [
|
||||
{ localId: "s.0", name: "s", type: STRING },
|
||||
{ localId: "search.0", name: "search", type: STRING },
|
||||
{ localId: "template.0", name: "template", type: STRING },
|
||||
{ localId: "position.0", name: "position", type: F64 },
|
||||
];
|
||||
const locals: IrLocal[] = [
|
||||
...params.map((param) => ({
|
||||
id: param.localId,
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
mutable: false,
|
||||
})),
|
||||
{ id: "chunks.0", name: "chunks", type: chunksType, mutable: false },
|
||||
{ id: "cursor.0", name: "cursor", type: F64, mutable: true },
|
||||
{ id: "dollar.0", name: "dollar", type: F64, mutable: false },
|
||||
{ id: "next.0", name: "next", type: STRING, mutable: false },
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: STRING,
|
||||
locals,
|
||||
body: [
|
||||
builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }),
|
||||
builder.declare("cursor.0", numLit(0, loc)),
|
||||
{
|
||||
kind: "while",
|
||||
cond: builder.compare("<", cursor, builder.length(template)),
|
||||
body: [
|
||||
builder.declare("dollar.0", {
|
||||
kind: "strIntrinsic",
|
||||
method: "indexOf",
|
||||
receiver: template,
|
||||
args: [strLit("$", loc), cursor],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
{
|
||||
kind: "if",
|
||||
cond: builder.compare("<", dollar, numLit(0, loc)),
|
||||
then: [{ kind: "break", loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
builder.append(builder.slice(template, cursor, dollar)),
|
||||
builder.assign("cursor.0", builder.add(dollar, numLit(1, loc))),
|
||||
builder.declare("next.0", {
|
||||
kind: "strIntrinsic",
|
||||
method: "charAt",
|
||||
receiver: template,
|
||||
args: [cursor],
|
||||
type: STRING,
|
||||
loc,
|
||||
}),
|
||||
...selection,
|
||||
],
|
||||
loc,
|
||||
},
|
||||
builder.append(builder.slice(template, cursor, undefined)),
|
||||
{ kind: "return", value: builder.join(), loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
function stringBuilder(loc: SrcLoc, chunks: IrExpr) {
|
||||
return {
|
||||
declare(localId: string, init: IrExpr): IrStmt {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Every identifier spelled `text` in `file`, indexed once per file;
|
||||
* callers resolve only the names of the binding they examine. Type
|
||||
* positions and shorthand properties are included, so a caller that
|
||||
* requires every use to have a known shape treats them conservatively. */
|
||||
export function identifiersNamed(
|
||||
lowerer: Lowerer,
|
||||
file: ts.SourceFile,
|
||||
text: string,
|
||||
): readonly ts.Identifier[] {
|
||||
let names = lowerer.splitIdentifierUses.get(file);
|
||||
if (!names) {
|
||||
names = new Map();
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!ts.isIdentifier(node)) return;
|
||||
const uses = names!.get(node.text);
|
||||
if (uses) uses.push(node);
|
||||
else names!.set(node.text, [node]);
|
||||
});
|
||||
lowerer.splitIdentifierUses.set(file, names);
|
||||
}
|
||||
return names.get(text) ?? [];
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { stripValueWrappers } from "./binding-analysis.js";
|
||||
import { identifiersNamed } from "./identifier-uses.js";
|
||||
|
||||
/** A fresh split array is dense. A stored result has the same guarantee
|
||||
* only when this loop is its sole observation: even a const array can be
|
||||
@@ -44,21 +45,7 @@ export function isPrivateSplitConsumer(
|
||||
)
|
||||
return false;
|
||||
|
||||
// Index spelling once per file; resolve only this binding's names. Type
|
||||
// positions and shorthand properties conservatively count as uses too.
|
||||
const file = decl.getSourceFile();
|
||||
let names = lowerer.splitIdentifierUses.get(file);
|
||||
if (!names) {
|
||||
names = new Map();
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!ts.isIdentifier(node)) return;
|
||||
const uses = names!.get(node.text);
|
||||
if (uses) uses.push(node);
|
||||
else names!.set(node.text, [node]);
|
||||
});
|
||||
lowerer.splitIdentifierUses.set(file, names);
|
||||
}
|
||||
for (const use of names.get(name.text) ?? []) {
|
||||
for (const use of identifiersNamed(lowerer, decl.getSourceFile(), name.text)) {
|
||||
if (use === decl.name || use === name) continue;
|
||||
if (lowerer.resolveValueSymbol(use) === symbol) return false;
|
||||
}
|
||||
|
||||
@@ -526,6 +526,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for
|
||||
* Latin-script text (scr_str_locale_compare). Never throws. */
|
||||
"str.localeCompare": { argTypes: [STRING, STRING], result: F64 },
|
||||
"str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING },
|
||||
/* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c):
|
||||
* trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the
|
||||
* whole span must be one StrNumericLiteral — signed decimal (Infinity
|
||||
|
||||
@@ -48,6 +48,7 @@ export const REGEX_INTRINSIC_SIGS: Record<
|
||||
// checks the union's arms.
|
||||
match: { receiver: STRING, argTypes: [REGEX], result: VOID },
|
||||
exec: { receiver: STRING, argTypes: [REGEX], result: VOID },
|
||||
execGroups: { receiver: STRING, argTypes: [REGEX, F64], result: VOID },
|
||||
lastIndex: { receiver: REGEX, argTypes: [], result: F64 },
|
||||
matchAll: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) },
|
||||
matchAllInto: {
|
||||
|
||||
@@ -2183,6 +2183,12 @@ export type IrRegexIntrinsicMethod =
|
||||
/** exec uses the same string-first operands but keeps the g/y
|
||||
* lastIndex refusal, independently of global String.match iteration. */
|
||||
| "exec"
|
||||
/** exec whose result is read only by null tests and constant-index
|
||||
* element reads: args[1] is a numeric literal whose bit k marks capture k
|
||||
* as read (k < 53). Unmarked captures read as undefined and the result
|
||||
* carries no index/input properties; the match, lastIndex transitions
|
||||
* and null result are exec's. */
|
||||
| "execGroups"
|
||||
| "lastIndex"
|
||||
/** `s.matchAll(re)` — every match as its honest string[] slice (match's
|
||||
* rule), drained EAGERLY into a fresh string[][]: the lazy iterator is
|
||||
|
||||
@@ -1949,7 +1949,10 @@ function validateFunction(
|
||||
const want = sig.argTypes[i];
|
||||
if (want) expectType(a, want, `regexIntrinsic ${e.method} arg ${i}`);
|
||||
});
|
||||
if (e.method === "match" || e.method === "exec") {
|
||||
if (e.method === "execGroups" && e.args[1]?.kind !== "numLit") {
|
||||
err("regexIntrinsic execGroups mask must be a numeric literal", e.loc);
|
||||
}
|
||||
if (e.method === "match" || e.method === "exec" || e.method === "execGroups") {
|
||||
// The `string[] | null` union (program-dependent id) — checked
|
||||
// by arms, like the libCall case checks process.envGet.
|
||||
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4890-regex-literal-sites.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4891-regex-plain-text-patterns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4892-regex-exec-read-captures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -35,6 +35,19 @@ void *lre_realloc(void *opaque, void *ptr, size_t size) {
|
||||
return realloc(ptr, size);
|
||||
}
|
||||
|
||||
/* The plain-text form of a pattern rides on its matcher (see
|
||||
* scr_regex_plain_text): NULL when there is none. */
|
||||
static inline const ScrStr *scr_regex_lit(const ScrRegex *re) {
|
||||
const ScrReNative *native = re->native;
|
||||
return native ? (const ScrStr *)native->lit : NULL;
|
||||
}
|
||||
|
||||
static void scr_regex_native_free(void *native) {
|
||||
if (!native) return;
|
||||
free(((ScrReNative *)native)->lit);
|
||||
scr_re_native_free(native);
|
||||
}
|
||||
|
||||
static SCR_TL ScrRegex **scr_compiled = NULL;
|
||||
static SCR_TL size_t scr_compiled_len = 0, scr_compiled_cap = 0;
|
||||
|
||||
@@ -42,7 +55,7 @@ static void scr_regex_free_bytecodes(void) {
|
||||
for (size_t i = 0; i < scr_compiled_len; i++) {
|
||||
lre_realloc(lre_opaque(), scr_compiled[i]->bc, 0);
|
||||
scr_compiled[i]->bc = NULL;
|
||||
scr_re_native_free(scr_compiled[i]->native);
|
||||
scr_regex_native_free(scr_compiled[i]->native);
|
||||
scr_compiled[i]->native = NULL;
|
||||
}
|
||||
free(scr_compiled);
|
||||
@@ -128,6 +141,54 @@ static char *scr_pattern_cesu8(const ScrStr *src, size_t *plen) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A pattern that matches exactly its own text: printable ASCII with no
|
||||
* syntax characters (an escaped syntax character or '/' stands for
|
||||
* itself), no sticky anchoring, and no case folding unless it has no
|
||||
* letters. Multiline, dotAll, global and unicode do not change what such a
|
||||
* pattern matches. Every byte match of an ASCII needle in well-formed UTF-8
|
||||
* is a code-unit match at the same position. Returns an immortal string
|
||||
* owned by the matcher (freed with it), or NULL. */
|
||||
static ScrStr *scr_regex_plain_text(const ScrRegex *re) {
|
||||
const ScrStr *flags = re->flags;
|
||||
bool fold = false;
|
||||
for (size_t i = 0; i < flags->len; i++) {
|
||||
if (flags->data[i] == 'y') return NULL;
|
||||
if (flags->data[i] == 'i') fold = true;
|
||||
}
|
||||
const ScrStr *src = re->source;
|
||||
if (src->len == 0 || src->len > 256) return NULL;
|
||||
char text[256];
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < src->len; i++) {
|
||||
unsigned char c = (unsigned char)src->data[i];
|
||||
if (c == '\\') {
|
||||
if (i + 1 >= src->len) return NULL;
|
||||
c = (unsigned char)src->data[++i];
|
||||
if (!strchr("^$\\.*+?()[]{}|/", c) || c == '\0') return NULL;
|
||||
} else if (c < 0x20 || c >= 0x7F || strchr("^$.*+?()[]{}|", c)) {
|
||||
return NULL;
|
||||
}
|
||||
if (fold && ((c | 0x20) >= 'a' && (c | 0x20) <= 'z')) return NULL;
|
||||
text[n++] = (char)c;
|
||||
}
|
||||
ScrStr *lit = malloc(sizeof(ScrStr) + n + 1);
|
||||
if (!lit) scr_regex_oom();
|
||||
memset(lit, 0, sizeof(ScrStr));
|
||||
lit->rc = SIZE_MAX;
|
||||
lit->len = n;
|
||||
lit->cap = (uint32_t)n;
|
||||
lit->ascii = 1;
|
||||
memcpy(lit->data, text, n);
|
||||
lit->data[n] = '\0';
|
||||
return lit;
|
||||
}
|
||||
|
||||
static void *scr_regex_native_build(const ScrRegex *re, const uint8_t *bc) {
|
||||
ScrReNative *native = scr_re_native_build(bc);
|
||||
if (native) native->lit = scr_regex_plain_text(re);
|
||||
return native;
|
||||
}
|
||||
|
||||
/* The compiled bytecode, compiling (and caching) on first use. */
|
||||
static uint8_t *scr_regex_bc(ScrRegex *re) {
|
||||
if (re->bc) return re->bc;
|
||||
@@ -159,7 +220,7 @@ static uint8_t *scr_regex_bc(ScrRegex *re) {
|
||||
re->source->data, re->flags->data, error_msg);
|
||||
}
|
||||
re->bc = bc;
|
||||
re->native = scr_re_native_build(bc);
|
||||
re->native = scr_regex_native_build(re, bc);
|
||||
scr_note_compiled(re);
|
||||
return bc;
|
||||
}
|
||||
@@ -175,7 +236,7 @@ void scr_regex_release(ScrRegex *re) {
|
||||
scr_str_release(re->flags);
|
||||
if (re->bc && !re->literal) {
|
||||
lre_realloc(lre_opaque(), re->bc, 0);
|
||||
scr_re_native_free(re->native);
|
||||
scr_regex_native_free(re->native);
|
||||
}
|
||||
free(re);
|
||||
}
|
||||
@@ -525,9 +586,11 @@ static int scr_regex_start(ScrRegex *re, int len) {
|
||||
/* A predicate needs only boundaries; it never materializes result strings. */
|
||||
bool scr_regex_test(ScrRegex *re, ScrStr *s) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
bool stateful = (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) != 0;
|
||||
const ScrStr *lit = scr_regex_lit(re);
|
||||
if (lit && !stateful) return scr_str_find_bytes(s->data, s->len, lit->data, lit->len) != NULL;
|
||||
ScrRegexInput input;
|
||||
scr_regex_input_init_cached(&input, re, bc, s);
|
||||
bool stateful = (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) != 0;
|
||||
int pos = stateful ? scr_regex_start(re, input.len) : 0;
|
||||
int rc = pos < 0 ? 0 : scr_exec(&input, bc, pos);
|
||||
if (stateful) re->last_index = rc == 1
|
||||
@@ -543,7 +606,7 @@ bool scr_regex_test(ScrRegex *re, ScrStr *s) {
|
||||
* and sticky regexes start at lastIndex, set it to the match end, and
|
||||
* reset it on failure. Nonparticipating captures are present undefined
|
||||
* elements, distinct from participating empty captures and array holes. */
|
||||
ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) {
|
||||
static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool metadata) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
bool stateful = (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) != 0;
|
||||
ScrRegexInput input;
|
||||
@@ -558,13 +621,15 @@ ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) {
|
||||
if (rc == 1) {
|
||||
int count = lre_get_capture_count(bc);
|
||||
out = scr_arr_new(SCR_ELEM_STR, (size_t)count);
|
||||
ScrDyn *metadata = scr_dyn_new_obj();
|
||||
scr_arr_ext(out)->metadata = metadata;
|
||||
scr_dyn_obj_set(metadata, "index", 5, scr_dyn_new_num((double)scr_capture_index(&input, capture[0])));
|
||||
scr_dyn_obj_set(metadata, "input", 5, scr_dyn_new_str(s));
|
||||
if (metadata) {
|
||||
ScrDyn *props = scr_dyn_new_obj();
|
||||
scr_arr_ext(out)->metadata = props;
|
||||
scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num((double)scr_capture_index(&input, capture[0])));
|
||||
scr_dyn_obj_set(props, "input", 5, scr_dyn_new_str(s));
|
||||
}
|
||||
for (int k = 0; k < count; k++) {
|
||||
const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
|
||||
if (cs == NULL || ce == NULL) {
|
||||
if (cs == NULL || ce == NULL || (k < 64 && !(mask >> k & 1))) {
|
||||
scr_arr_set_undefined(out, (double)out->len);
|
||||
} else {
|
||||
scr_arr_push_ref(
|
||||
@@ -576,6 +641,16 @@ ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) {
|
||||
return out; /* +1, or NULL (no match) */
|
||||
}
|
||||
|
||||
ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) {
|
||||
return scr_regex_exec_core(s, re, UINT64_MAX, true);
|
||||
}
|
||||
|
||||
/* Unselected captures are present undefined elements: nothing reads them. */
|
||||
ScrArr *scr_regex_exec_groups(ScrStr *s, ScrRegex *re, double mask) {
|
||||
uint64_t bits = mask >= 0 && mask < 18446744073709551616.0 ? (uint64_t)mask : UINT64_MAX;
|
||||
return scr_regex_exec_core(s, re, bits, false);
|
||||
}
|
||||
|
||||
/* Global Symbol.match always starts at zero and drains whole matches,
|
||||
* ignoring capture groups. Its terminating failed exec resets lastIndex
|
||||
* to zero, so no state escapes this internal iteration. Sticky globals
|
||||
@@ -614,6 +689,19 @@ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) {
|
||||
* bytecode omits the implicit forward scan) — exactly Node. */
|
||||
double scr_regex_search(ScrStr *s, ScrRegex *re) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
const ScrStr *lit = scr_regex_lit(re);
|
||||
if (lit) {
|
||||
const char *hit = scr_str_find_bytes(s->data, s->len, lit->data, lit->len);
|
||||
if (!hit) return -1;
|
||||
/* UTF-16 units before the match: one per UTF-8 lead byte, two for an
|
||||
* astral (4-byte) lead. */
|
||||
double units = 0;
|
||||
for (const char *p = s->data; p < hit; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
if ((c & 0xC0) != 0x80) units += c >= 0xF0 ? 2 : 1;
|
||||
}
|
||||
return units;
|
||||
}
|
||||
ScrRegexInput input;
|
||||
scr_regex_input_init(&input, re, bc, s);
|
||||
uint8_t **capture = input.capture;
|
||||
@@ -786,6 +874,13 @@ static ScrStr *scr_replace_impl(ScrStr *s, ScrRegex *re, ScrStr *rep) {
|
||||
bool global = (re_flags & LRE_FLAG_GLOBAL) != 0;
|
||||
bool sticky = (re_flags & LRE_FLAG_STICKY) != 0;
|
||||
bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
|
||||
const ScrStr *lit = scr_regex_lit(re);
|
||||
if (lit && !sticky) {
|
||||
/* A plain-text pattern has no captures, so its substitutions are the
|
||||
* string-search ones. */
|
||||
if (global) re->last_index = 0;
|
||||
return scr_str_replace_bytes(s, lit->data, lit->len, rep, global);
|
||||
}
|
||||
int capture_count = lre_get_capture_count(bc);
|
||||
const char *groupnames = lre_get_groupnames(bc);
|
||||
ScrRegexInput input;
|
||||
@@ -978,7 +1073,7 @@ ScrStr *scr_util_strip_vt(const ScrDyn *input) {
|
||||
source = { SIZE_MAX, sizeof SCR_VT_PATTERN - 1, sizeof SCR_VT_PATTERN - 1, SCR_VT_PATTERN };
|
||||
static const struct { size_t rc, len, cap; char data[2]; }
|
||||
flags = { SIZE_MAX, 1, 1, "g" };
|
||||
static SCR_TL ScrRegex template = { SIZE_MAX, (ScrStr *)&source, (ScrStr *)&flags, NULL, 0, NULL };
|
||||
static SCR_TL ScrRegex template = { SIZE_MAX, (ScrStr *)&source, (ScrStr *)&flags, NULL, 0, NULL, NULL };
|
||||
ScrRegex *regex = scr_regex_literal(&template);
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrStr *result = scr_regex_replace(subject, regex, empty);
|
||||
@@ -1004,6 +1099,10 @@ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep) {
|
||||
|
||||
ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit_num) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
/* Without captures or sticky probing, splitting on a plain-text pattern
|
||||
* is splitting on its text. */
|
||||
const ScrStr *lit = scr_regex_lit(re);
|
||||
if (lit) return scr_str_split_limit(s, (ScrStr *)lit, limit_num);
|
||||
uint32_t limit = scr_to_uint32(limit_num);
|
||||
if (limit == 0) return scr_arr_new(SCR_ELEM_STR, 0);
|
||||
int capture_count = lre_get_capture_count(bc);
|
||||
@@ -1544,7 +1643,7 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) {
|
||||
return NULL;
|
||||
}
|
||||
re->bc = bc;
|
||||
re->native = scr_re_native_build(bc);
|
||||
re->native = scr_regex_native_build(re, bc);
|
||||
scr_regex_ctor_remember(re);
|
||||
return re;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ typedef struct ScrReNative {
|
||||
bool first_bol; /* some path is anchored at position 0 */
|
||||
int first_byte; /* the only byte that can start a match, or -1 */
|
||||
uint8_t first[128];
|
||||
/* scr_regex.c: the ScrStr text of a pattern that matches only itself,
|
||||
* or NULL. Owned by the matcher; released before scr_re_native_free. */
|
||||
void *lit;
|
||||
} ScrReNative;
|
||||
|
||||
static uint32_t rn_u16(const uint8_t *p) {
|
||||
|
||||
@@ -1336,6 +1336,17 @@ ScrStr *scr_str_trim_end(ScrStr *s);
|
||||
* U+FFFD — divergence, see above); otherwise splits on every occurrence,
|
||||
* empty pieces kept. Borrows both; returns a +1 string[] (SCR_ELEM_STR). */
|
||||
struct ScrArr;
|
||||
/* String.prototype.replace/replaceAll with a string search value and a
|
||||
* string replacement: GetSubstitution without captures ($$, $&, $`, $';
|
||||
* every other '$' spelling stays literal). An empty search value matches
|
||||
* at 0, or at every UTF-16 position under replaceAll. Borrows; +1. */
|
||||
ScrStr *scr_str_replace_literal(ScrStr *s, ScrStr *search, ScrStr *rep, bool all);
|
||||
/* The same over a nonempty well-formed UTF-8 needle (literal regex
|
||||
* patterns share it). Borrows; +1. */
|
||||
ScrStr *scr_str_replace_bytes(ScrStr *s, const char *pat, size_t plen, ScrStr *rep, bool all);
|
||||
/* First occurrence of a nonempty needle in a byte span, or NULL. */
|
||||
const char *scr_str_find_bytes(const char *hay, size_t hay_len, const char *needle,
|
||||
size_t needle_len);
|
||||
struct ScrArr *scr_str_split(ScrStr *s, ScrStr *sep);
|
||||
struct ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit);
|
||||
/* Internal dense split traversal. The cursor borrows input strings, returns
|
||||
@@ -1913,6 +1924,11 @@ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re);
|
||||
/* re.exec(s), string-first ABI: match slice with global/sticky lastIndex
|
||||
* transitions. Borrows both; +1 or NULL. */
|
||||
ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re);
|
||||
/* exec for a result read only through constant indices and null tests:
|
||||
* bit k of `mask` (k < 53) selects the captures that are materialized; the
|
||||
* rest, and the index/input properties, are left out. Same lastIndex
|
||||
* transitions and NULL result as scr_regex_exec. Borrows both; +1 or NULL. */
|
||||
ScrArr *scr_regex_exec_groups(ScrStr *s, ScrRegex *re, double mask);
|
||||
/* s.search(re): the first match's UTF-16 index, or -1 — a fresh exec from
|
||||
* position 0 (Symbol.search never touches lastIndex, so no flag fence:
|
||||
* /g is irrelevant, /y anchors at 0). Borrows both; never throws. */
|
||||
|
||||
@@ -1796,6 +1796,126 @@ ScrArr *scr_str_split(ScrStr *s, ScrStr *sep) {
|
||||
return scr_str_split_limit(s, sep, 4294967295.0);
|
||||
}
|
||||
|
||||
/* ── replace / replaceAll with a string search value ──────────────────
|
||||
* One forward scan over the subject bytes. A nonempty needle is well-formed
|
||||
* UTF-8, so every byte match starts on a character boundary and is exactly
|
||||
* a UTF-16 match; the substitution spans are byte ranges of the subject. */
|
||||
|
||||
/* GetSubstitution with no captures: $$, $&, $` and $' expand. With no
|
||||
* capture groups and no named groups, $n and $<name> stay literal, as does
|
||||
* every other '$'. */
|
||||
static void scr_replace_template(ScrJsonBuf *b, const char *before, size_t before_len,
|
||||
const char *match, size_t match_len, const char *after,
|
||||
size_t after_len, const ScrStr *rep) {
|
||||
size_t i = 0;
|
||||
while (i < rep->len) {
|
||||
const char *dollar = memchr(rep->data + i, '$', rep->len - i);
|
||||
size_t stop = dollar ? (size_t)(dollar - rep->data) : rep->len;
|
||||
scr_jb_write(b, rep->data + i, stop - i);
|
||||
if (!dollar) return;
|
||||
i = stop;
|
||||
char next = i + 1 < rep->len ? rep->data[i + 1] : '\0';
|
||||
if (i + 1 < rep->len && (next == '$' || next == '&' || next == '`' || next == '\'')) {
|
||||
if (next == '$') scr_jb_putc(b, '$');
|
||||
else if (next == '&') scr_jb_write(b, match, match_len);
|
||||
else if (next == '`') scr_jb_write(b, before, before_len);
|
||||
else scr_jb_write(b, after, after_len);
|
||||
i += 2;
|
||||
} else {
|
||||
scr_jb_putc(b, '$');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char *scr_str_find_bytes(const char *hay, size_t hay_len, const char *needle,
|
||||
size_t needle_len) {
|
||||
return scr_byte_find(hay, hay_len, needle, needle_len);
|
||||
}
|
||||
|
||||
ScrStr *scr_str_replace_bytes(ScrStr *s, const char *pat, size_t plen, ScrStr *rep, bool all) {
|
||||
const char *end = s->data + s->len;
|
||||
const char *hit = scr_byte_find(s->data, s->len, pat, plen);
|
||||
if (!hit) return scr_str_retain(s);
|
||||
if (memchr(rep->data, '$', rep->len) != NULL) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
const char *from = s->data;
|
||||
while (hit) {
|
||||
size_t pos = (size_t)(hit - s->data);
|
||||
scr_jb_write(&b, from, (size_t)(hit - from));
|
||||
scr_replace_template(&b, s->data, pos, hit, plen, hit + plen, s->len - pos - plen, rep);
|
||||
from = hit + plen;
|
||||
if (!all) break;
|
||||
hit = scr_byte_find(from, (size_t)(end - from), pat, plen);
|
||||
}
|
||||
scr_jb_write(&b, from, (size_t)(end - from));
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
/* A plain replacement: count, size the result once, then copy. */
|
||||
size_t count = 1;
|
||||
if (all) {
|
||||
for (const char *p = scr_byte_find(hit + plen, (size_t)(end - hit - plen), pat, plen); p;
|
||||
p = scr_byte_find(p + plen, (size_t)(end - p - plen), pat, plen))
|
||||
count++;
|
||||
}
|
||||
size_t kept = s->len - count * plen;
|
||||
if (rep->len && count > (SCR_STR_MAX_CAP - kept) / rep->len) scr_oom();
|
||||
size_t total = kept + count * rep->len;
|
||||
ScrStr *out = scr_str_alloc_raw(total, total);
|
||||
char *w = out->data;
|
||||
const char *from = s->data;
|
||||
for (size_t k = 0; k < count; k++) {
|
||||
size_t gap = (size_t)(hit - from);
|
||||
memcpy(w, from, gap);
|
||||
w += gap;
|
||||
memcpy(w, rep->data, rep->len);
|
||||
w += rep->len;
|
||||
from = hit + plen;
|
||||
if (k + 1 < count) hit = scr_byte_find(from, (size_t)(end - from), pat, plen);
|
||||
}
|
||||
memcpy(w, from, (size_t)(end - from));
|
||||
out->data[total] = '\0';
|
||||
scr_str_hash_forget(out);
|
||||
if (s->ascii && rep->ascii) scr_str_note_ascii(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrStr *scr_str_replace_literal(ScrStr *s, ScrStr *search, ScrStr *rep, bool all) {
|
||||
if (search->len) return scr_str_replace_bytes(s, search->data, search->len, rep, all);
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
if (!all) {
|
||||
scr_replace_template(&b, s->data, 0, s->data, 0, s->data, s->len, rep);
|
||||
scr_jb_write(&b, s->data, s->len);
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
/* The empty needle matches at every UTF-16 position, including between
|
||||
* the halves of a surrogate pair; those spans follow slice's contract. */
|
||||
size_t len16 = (size_t)scr_str_utf16_len(s);
|
||||
if (len16 == s->len) {
|
||||
for (size_t k = 0; k <= s->len; k++) {
|
||||
scr_replace_template(&b, s->data, k, s->data + k, 0, s->data + k, s->len - k, rep);
|
||||
if (k < s->len) scr_jb_putc(&b, s->data[k]);
|
||||
}
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
for (size_t k = 0; k <= len16; k++) {
|
||||
ScrStr *before = scr_str_slice(s, 0, (double)k);
|
||||
ScrStr *after = scr_str_slice(s, (double)k, (double)len16);
|
||||
scr_replace_template(&b, before->data, before->len, after->data, 0, after->data, after->len,
|
||||
rep);
|
||||
scr_str_release(before);
|
||||
scr_str_release(after);
|
||||
if (k < len16) {
|
||||
ScrStr *unit = scr_str_slice(s, (double)k, (double)(k + 1));
|
||||
scr_jb_put_str(&b, unit);
|
||||
scr_str_release(unit);
|
||||
}
|
||||
}
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
|
||||
/* StringPad (ECMA-262 22.1.3.16/17): target length in UTF-16 units, the
|
||||
* filler built from whole repetitions of `fill` plus a truncated prefix.
|
||||
* A target at or below the length (or an empty fill) returns the receiver
|
||||
|
||||
@@ -324,6 +324,51 @@ static void test_capture_capacity(void) {
|
||||
expect_str(capture, "\xF0\x9F\x98\x80", "capture owns its bytes after subject release");
|
||||
}
|
||||
|
||||
static void test_exec_groups(void) {
|
||||
/* Only the selected captures are materialized; the rest read undefined. */
|
||||
ScrStr *s = S("xac");
|
||||
ScrArr *full = scr_regex_exec(s, &re_opt_group);
|
||||
ScrArr *lean = scr_regex_exec_groups(s, &re_opt_group, 2.0);
|
||||
check(full && lean && full->len == lean->len && lean->len == 2, "exec_groups keeps the capture count");
|
||||
check(lean->present[0] == SCR_ARR_UNDEFINED && lean->present[1] == SCR_ARR_UNDEFINED,
|
||||
"exec_groups: unselected and nonparticipating captures are undefined");
|
||||
check(lean->ext == NULL || lean->ext->metadata == NULL, "exec_groups: no index/input properties");
|
||||
scr_arr_release(full);
|
||||
scr_arr_release(lean);
|
||||
ScrStr *ab = S("ab");
|
||||
lean = scr_regex_exec_groups(ab, &re_group, 2.0);
|
||||
ScrStr *group = lean ? scr_arr_get_ref(lean, 1) : NULL;
|
||||
check(group && str_is(group, "b"), "exec_groups: a selected capture keeps its value");
|
||||
scr_str_release(group);
|
||||
scr_arr_release(lean);
|
||||
check(scr_regex_exec_groups(s, &re_group, 1.0) == NULL, "exec_groups: no match is NULL");
|
||||
scr_str_release(ab);
|
||||
scr_str_release(s);
|
||||
}
|
||||
|
||||
static void test_string_replace(void) {
|
||||
/* String search values: one pass, GetSubstitution without captures. */
|
||||
ScrStr *s = S("a.b.c");
|
||||
ScrStr *dot = S(".");
|
||||
ScrStr *empty = S("");
|
||||
ScrStr *rep = S("<$&|$`|$'|$1|$$>");
|
||||
ScrStr *plain = S("--");
|
||||
expect_str(scr_str_replace_literal(s, dot, plain, false), "a--b.c", "string replace: first match");
|
||||
expect_str(scr_str_replace_literal(s, dot, plain, true), "a--b--c", "string replaceAll: every match");
|
||||
expect_str(scr_str_replace_literal(s, dot, rep, false), "a<.|a|b.c|$1|$>b.c",
|
||||
"string replace: substitutions");
|
||||
expect_str(scr_str_replace_literal(s, empty, plain, true), "--a--.--b--.--c--",
|
||||
"string replaceAll: empty search at every position");
|
||||
expect_str(scr_str_replace_literal(s, plain, dot, true), "a.b.c", "string replaceAll: no match");
|
||||
expect_str(scr_regex_replace(s, &re_dot_g, plain), "a--b--c", "plain-text regex replace");
|
||||
check(scr_regex_search(s, &re_dot_noglobal) == 1, "plain-text regex search");
|
||||
scr_str_release(s);
|
||||
scr_str_release(dot);
|
||||
scr_str_release(empty);
|
||||
scr_str_release(rep);
|
||||
scr_str_release(plain);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
@@ -341,6 +386,8 @@ int main(int argc, char **argv) {
|
||||
test_split();
|
||||
test_borrowed_results();
|
||||
test_capture_capacity();
|
||||
test_exec_groups();
|
||||
test_string_replace();
|
||||
|
||||
#ifdef SCR_RC_AUDIT
|
||||
check(scr_str_live_count() == 0, "no live heap strings at exit (RC audit)");
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Regex literals evaluated directly as the regex of test/exec/match/
|
||||
// matchAll/search/replace/replaceAll/split: each evaluation starts from a
|
||||
// fresh lastIndex, so global and sticky sites behave the same on every
|
||||
// pass, including when another operand evaluates the same site first.
|
||||
export {};
|
||||
|
||||
const subjects = ["a1b22c333", "", "xyz", "aaa", "é1😀22漢333"];
|
||||
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
const row: string[] = [];
|
||||
for (const s of subjects) {
|
||||
row.push(String(/\d+/g.test(s)));
|
||||
row.push(String(/\d/y.test(s)));
|
||||
row.push(String(/a/g.exec(s)?.[0]));
|
||||
row.push(String(/\d+/y.exec(s)?.[0]));
|
||||
row.push(JSON.stringify(s.match(/\d+/g)));
|
||||
row.push(JSON.stringify(s.match(/[a-z]/y)));
|
||||
row.push(String(s.search(/\d/g)));
|
||||
row.push(String(s.search(/\d/y)));
|
||||
row.push(s.replace(/\d/g, "#"));
|
||||
row.push(s.replace(/\d/y, "#"));
|
||||
row.push(s.replace(/a/, "<$&>"));
|
||||
row.push(s.replaceAll(/\d+/g, "[$&]"));
|
||||
row.push(JSON.stringify(s.split(/\d+/)));
|
||||
row.push(JSON.stringify(s.split(/(\d)/, 3)));
|
||||
row.push(JSON.stringify([...s.matchAll(/(\d)(\d)?/g)]));
|
||||
}
|
||||
console.log(pass, row.join(" | "));
|
||||
}
|
||||
|
||||
// A loop over one global literal site: every evaluation is a new object.
|
||||
let found = 0;
|
||||
for (let i = 0; i < 5; i++) if (/b/g.test("abc")) found++;
|
||||
console.log("found", found);
|
||||
|
||||
// Another operand evaluates the same site before the call itself runs.
|
||||
let depth = 0;
|
||||
function probe(s: string): string {
|
||||
if (depth < 3) {
|
||||
depth++;
|
||||
console.log("inner", depth, check(s));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function check(s: string): boolean {
|
||||
return /x/g.test(probe(s));
|
||||
}
|
||||
console.log("outer", check("xx"));
|
||||
|
||||
depth = 0;
|
||||
function sticky(s: string): string | undefined {
|
||||
return /x/y.exec(stickyProbe(s))?.[0];
|
||||
}
|
||||
function stickyProbe(s: string): string {
|
||||
if (depth < 2) {
|
||||
depth++;
|
||||
console.log("sticky inner", sticky(s));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
console.log("sticky outer", sticky("xx"));
|
||||
|
||||
// Stored literals keep their own identity and lastIndex.
|
||||
const made: RegExp[] = [];
|
||||
for (let i = 0; i < 3; i++) made.push(/q/g);
|
||||
console.log(made[0] === made[1], made[1] === made[2]);
|
||||
made[0]!.test("qq");
|
||||
console.log(made[0]!.lastIndex, made[1]!.lastIndex);
|
||||
const kept = /q/g;
|
||||
kept.test("qq");
|
||||
console.log(kept.lastIndex, kept.test("qq"), kept.lastIndex, kept.test("qq"), kept.lastIndex);
|
||||
console.log(/q/g.lastIndex, /q/g.source, /q/g.flags, String(/q/gi));
|
||||
|
||||
// Callback replacements on literal sites.
|
||||
console.log("a1a2".replace(/a(\d)/g, (m: string, d: string) => `<${m}:${d}>`));
|
||||
console.log("a1a2".replace(/a/y, () => "Y"));
|
||||
@@ -0,0 +1,64 @@
|
||||
// Patterns that match only their own text: substitutions, flags, escapes,
|
||||
// non-ASCII subjects, split limits and constructed regexes.
|
||||
export {};
|
||||
|
||||
const subjects = ["", "a.b.a", "x$y$$z", "a/b//c", "aaaa", "é.漢.😀.", "...", "a\0.b", "Tab\tq"];
|
||||
const templates = ["", "-", "$", "$$", "$&", "$`", "$'", "$1", "$0", "$<n>", "<$&|$`|$'>", "é😀$$"];
|
||||
|
||||
for (const s of subjects) {
|
||||
const parts: string[] = [];
|
||||
for (const t of templates) {
|
||||
parts.push(s.replace(/\./, t), s.replace(/\./g, t), s.replaceAll(/\./g, t));
|
||||
}
|
||||
console.log(JSON.stringify(s), JSON.stringify(parts));
|
||||
console.log(
|
||||
s.replace(/\$/g, "D"),
|
||||
s.replace(/\//g, "|"),
|
||||
s.replace(/a/g, "A"),
|
||||
s.replace(/aa/g, "b"),
|
||||
s.replace(/\//, "!"),
|
||||
s.replace(/\t/g, "T"),
|
||||
);
|
||||
console.log(
|
||||
/\./.test(s),
|
||||
/\$\$/.test(s),
|
||||
/a\.b/.test(s),
|
||||
s.search(/\./),
|
||||
s.search(/a/),
|
||||
s.search(/😀/u),
|
||||
s.search(/漢/),
|
||||
);
|
||||
for (const limit of [undefined, 0, 1, 2, -1, 4294967297, 1.5, NaN]) {
|
||||
console.log(limit, JSON.stringify(s.split(/\./, limit)), JSON.stringify(s.split(/\.\./, limit)));
|
||||
}
|
||||
}
|
||||
|
||||
// Flags that do not change a plain pattern, and ones that do.
|
||||
const text = "1-2\n-3-A-a";
|
||||
console.log(text.replace(/-/gi, "+"), text.replace(/-/gm, "+"), text.replace(/-/gs, "+"), text.replace(/-/gu, "+"));
|
||||
console.log(text.replace(/a/gi, "+"), text.replace(/A/g, "+"), text.replace(/-/y, "+"), text.replace(/1/y, "+"));
|
||||
console.log(text.split(/-/i), text.split(/-/y), text.search(/-/i), /\n/m.test(text), /3/i.test(text));
|
||||
|
||||
// Constructed regexes over the same patterns.
|
||||
const dots = new RegExp("\\.", "g");
|
||||
console.log("a.b.c".replace(dots, "!"), dots.lastIndex, dots.test("a.b"), dots.lastIndex);
|
||||
console.log("a.b.c".split(new RegExp("\\.")), "a.b.c".search(new RegExp("b")), new RegExp("x", "g").test("yx"));
|
||||
|
||||
// Global test and exec keep lastIndex semantics on stored plain patterns.
|
||||
const comma = /,/g;
|
||||
const csv = "a,b,,c";
|
||||
const ends: number[] = [];
|
||||
while (comma.test(csv)) ends.push(comma.lastIndex);
|
||||
console.log(ends.join(" "), comma.lastIndex);
|
||||
let m: RegExpExecArray | null;
|
||||
const plain = /,/g;
|
||||
while ((m = plain.exec("é,😀,x")) !== null) console.log(m[0], m.index, plain.lastIndex);
|
||||
const once = /b/;
|
||||
once.lastIndex = 5;
|
||||
console.log(once.test("abc"), once.lastIndex, "abc".replace(once, "B"), once.lastIndex);
|
||||
|
||||
// Long subjects and many matches.
|
||||
const long = "<&>\"".repeat(2000) + "é".repeat(100) + "&";
|
||||
const escaped = long.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
console.log(escaped.length, escaped.slice(0, 40), escaped.slice(-20));
|
||||
console.log(long.split(/&/).length, long.split(/&/, 7).length);
|
||||
@@ -0,0 +1,59 @@
|
||||
// exec results read only through null tests and constant indices, beside
|
||||
// results that are also read in other ways: captures that are read keep
|
||||
// their values, nonparticipating captures stay undefined, and lastIndex
|
||||
// moves exactly as with a full result.
|
||||
export {};
|
||||
|
||||
const line = /^(\w+) (\d+)(?: (x))?(?: (y))?$/;
|
||||
const inputs = ["get 12 x", "put 7", "del 3 x y", "nope", "post 99 y", ""];
|
||||
for (const input of inputs) {
|
||||
const m = line.exec(input);
|
||||
if (m === null) {
|
||||
console.log("no match", JSON.stringify(input));
|
||||
continue;
|
||||
}
|
||||
console.log(m[1], m[2]!.length, m[3], m[4] ?? "-", m[9]);
|
||||
}
|
||||
|
||||
for (const input of inputs) {
|
||||
const m = /^(\w+) (\d+)/.exec(input);
|
||||
if (m !== null) console.log("lit", m[2], m[1]!.toUpperCase());
|
||||
}
|
||||
|
||||
// The same pattern with full results: length, index, input, spreading.
|
||||
for (const input of inputs) {
|
||||
const m = line.exec(input);
|
||||
if (m !== null) console.log(m.length, m.index, m.input, JSON.stringify([...m]), m.join("/"));
|
||||
}
|
||||
|
||||
// A global regex: lastIndex still follows each match.
|
||||
const word = /([a-z])([a-z]*)/g;
|
||||
const sentence = "ab cde é fgh";
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const m = word.exec(sentence);
|
||||
console.log(m === null ? "null" : m[1], word.lastIndex);
|
||||
}
|
||||
const sticky = /(\d)/y;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const m = sticky.exec("12a3");
|
||||
console.log(m != null ? m[1] : "none", sticky.lastIndex);
|
||||
}
|
||||
|
||||
// Results that escape keep everything.
|
||||
function first(s: string): RegExpExecArray | null {
|
||||
const m = /(\d)(\d)?/.exec(s);
|
||||
return m;
|
||||
}
|
||||
const kept = first("a5");
|
||||
console.log(kept !== null ? JSON.stringify([kept.index, kept[0], kept[1], kept[2]]) : "null");
|
||||
const stored: (string | undefined)[] = [];
|
||||
const all = /(a)|(b)/.exec("b");
|
||||
if (all !== null) stored.push(all[0], all[1], all[2]);
|
||||
console.log(JSON.stringify(stored), stored.length);
|
||||
const late = /(z)(q)?/.exec("zz");
|
||||
if (late !== null) {
|
||||
const read = () => late[1] + String(late[2]);
|
||||
console.log(read());
|
||||
}
|
||||
const groups = /(?<year>\d{4})-(?<month>\d{2})/.exec("on 2026-10");
|
||||
if (groups !== null) console.log(groups[1], groups.groups?.year, groups.index);
|
||||
Reference in New Issue
Block a user