mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Give matchAll rows and exec results their index, input and groups
Match results record their metadata lazily. The results of one exec or matchAll call share a source (the subject and the regex), and each result keeps its start index. index, input and a null-prototype groups object are built when a result is converted to a dynamic value; .index and .input on a match slice read the source directly. matchAll rows are allocated inline, like exec results. A drain whose rows are read only through their elements, .length, the .groups projection and the companion index uses matchAllCaptures or matchAllCapturesInto, whose rows carry no metadata.
This commit is contained in:
@@ -147,7 +147,9 @@ function directRegexOperandMethod(method: string): boolean {
|
||||
case "match":
|
||||
case "search":
|
||||
case "matchAll":
|
||||
case "matchAllCaptures":
|
||||
case "matchAllInto":
|
||||
case "matchAllCapturesInto":
|
||||
case "replace":
|
||||
case "replaceAll":
|
||||
case "split":
|
||||
@@ -192,7 +194,7 @@ export function emitRegexIntrinsic(
|
||||
} else {
|
||||
values = preservesRegexInputs(method)
|
||||
? emitBorrowedInputs(host, inputs)
|
||||
: method === "matchAllInto"
|
||||
: method === "matchAllInto" || method === "matchAllCapturesInto"
|
||||
? inputs.map((a) => emitBorrowedInput(host, a))
|
||||
: inputs.map((a) => host.emitExpr(a));
|
||||
}
|
||||
@@ -216,6 +218,9 @@ export function emitRegexIntrinsic(
|
||||
// Every match drained eagerly into a fresh +1 string[][]; throws
|
||||
// Node's TypeError on a non-global regex (catchable).
|
||||
return fallible("scr_regex_match_all", `ptr ${r.name}, ptr ${args[0]!.name}`);
|
||||
case "matchAllCaptures":
|
||||
// matchAll whose rows carry no index, input or groups properties.
|
||||
return fallible("scr_regex_match_all_captures", `ptr ${r.name}, ptr ${args[0]!.name}`);
|
||||
case "matchAllInto":
|
||||
// matchAll's companion-index form: args[1] (a number[]) also
|
||||
// receives each match's UTF-16 start index.
|
||||
@@ -223,6 +228,11 @@ export function emitRegexIntrinsic(
|
||||
"scr_regex_match_all_into",
|
||||
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
|
||||
);
|
||||
case "matchAllCapturesInto":
|
||||
return fallible(
|
||||
"scr_regex_match_all_captures_into",
|
||||
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
|
||||
);
|
||||
case "replaceAll":
|
||||
return fallible(
|
||||
"scr_regex_replace_all",
|
||||
|
||||
@@ -1394,6 +1394,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"regex.groupEquals": "scr_regex_group_eq",
|
||||
"regex.groupNumber": "scr_regex_group_number",
|
||||
"regex.groupParseInt": "scr_regex_group_parse_int",
|
||||
"regex.resultProp": "scr_regex_result_prop",
|
||||
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
|
||||
// throws synchronously), the minted setImmediate value, and the
|
||||
// timers/promises immediate — all mark the loop live.
|
||||
|
||||
@@ -29,6 +29,7 @@ export function preservesRegexInputs(method: string): boolean {
|
||||
case "match":
|
||||
case "search":
|
||||
case "matchAll":
|
||||
case "matchAllCaptures":
|
||||
case "replace":
|
||||
case "replaceAll":
|
||||
case "split":
|
||||
|
||||
@@ -104,6 +104,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"regex.groupEquals",
|
||||
"regex.groupNumber",
|
||||
"regex.groupParseInt",
|
||||
"regex.resultProp",
|
||||
"process.cwd",
|
||||
"process.argv",
|
||||
"process.execArgv",
|
||||
|
||||
@@ -297,7 +297,9 @@ export function computeMayThrow(
|
||||
rec.method === "replaceAll" ||
|
||||
rec.method === "split" ||
|
||||
rec.method === "matchAll" ||
|
||||
rec.method === "matchAllInto"
|
||||
rec.method === "matchAllCaptures" ||
|
||||
rec.method === "matchAllInto" ||
|
||||
rec.method === "matchAllCapturesInto"
|
||||
) {
|
||||
f.throws = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { identifiersNamed } from "../identifier-uses.js";
|
||||
import { bindingNeverReassigned } from "../binding-analysis.js";
|
||||
import { matchCallbackParamGroupsOf, matchResultNamedGroupsOf } from "../lower-exprs.js";
|
||||
|
||||
/** Whether the rows a matchAll drain produces are read only through their
|
||||
* elements: element reads and `in` tests with number keys, `.length`, the compile-time
|
||||
* `.groups` projection, `{ groups }` destructuring and, on a for-of binding
|
||||
* over the drain itself (`companion`), the companion-index `.index`. Rows
|
||||
* reach these reads through for-of loops, spreads, `Array.from`, stored
|
||||
* const bindings, `filter`/`slice`/`toReversed` copies, `find`/`findLast`/
|
||||
* `at` picks and inline callbacks of the array methods below. No such read
|
||||
* can observe a row's `index`, `input` or `groups` properties, so the
|
||||
* drain leaves them out. False for any other use. */
|
||||
export function matchAllRowsReadByElements(
|
||||
lowerer: Lowerer,
|
||||
drain: ts.Expression,
|
||||
companion: boolean,
|
||||
): boolean {
|
||||
const parent = outerValue(drain).parent;
|
||||
if (
|
||||
ts.isCallExpression(parent) &&
|
||||
parent.arguments[0] === outerValue(drain) &&
|
||||
ts.isPropertyAccessExpression(parent.expression) &&
|
||||
parent.expression.name.text === "from" &&
|
||||
lowerer.isStdlibGlobal(parent.expression.expression, "Array")
|
||||
) {
|
||||
const mapper = parent.arguments[1];
|
||||
if (mapper === undefined) return rowsReadByElements(lowerer, parent, 0);
|
||||
return parent.arguments.length === 2 && callbackReadsByElements(lowerer, mapper, 2);
|
||||
}
|
||||
if (ts.isForOfStatement(parent) && parent.expression === outerValue(drain))
|
||||
return forOfReadsByElements(lowerer, parent, companion);
|
||||
return rowsReadByElements(lowerer, drain, 0);
|
||||
}
|
||||
|
||||
/** matchAllRowsReadByElements for a stored drain, `const rows =
|
||||
* s.matchAll(re)`: a const-binding for-of loop over `rows` in the same
|
||||
* function walks the companion indices, so its binding may also read
|
||||
* `.index`. */
|
||||
export function storedMatchAllRowsReadByElements(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): boolean {
|
||||
const scope = enclosingFunction(decl);
|
||||
return constBindingReadsByElements(lowerer, decl, (use) => {
|
||||
const parent = outerValue(use).parent;
|
||||
if (
|
||||
ts.isForOfStatement(parent) &&
|
||||
parent.expression === outerValue(use) &&
|
||||
ts.isVariableDeclarationList(parent.initializer) &&
|
||||
(parent.initializer.flags & ts.NodeFlags.Const) !== 0 &&
|
||||
enclosingFunction(parent) === scope
|
||||
)
|
||||
return forOfReadsByElements(lowerer, parent, true);
|
||||
return rowsReadByElements(lowerer, use, 1);
|
||||
});
|
||||
}
|
||||
|
||||
function enclosingFunction(node: ts.Node): ts.Node | undefined {
|
||||
let at = node.parent;
|
||||
while (at !== undefined && !ts.isFunctionLike(at)) at = at.parent;
|
||||
return at;
|
||||
}
|
||||
|
||||
/** Array methods whose first argument is a callback over the rows. */
|
||||
const ROW_CALLBACK_METHODS = new Set([
|
||||
"map",
|
||||
"forEach",
|
||||
"filter",
|
||||
"some",
|
||||
"every",
|
||||
"find",
|
||||
"findLast",
|
||||
"findIndex",
|
||||
"findLastIndex",
|
||||
"flatMap",
|
||||
]);
|
||||
|
||||
/** Array methods whose result holds rows of the receiver. */
|
||||
const ROW_SUBSET_METHODS = new Set(["filter", "slice", "toReversed"]);
|
||||
|
||||
/** Array methods that return one row of the receiver. */
|
||||
const ROW_PICK_METHODS = new Set(["find", "findLast", "at"]);
|
||||
|
||||
/** Bounds the walk through stored bindings and copies. */
|
||||
const MAX_DEPTH = 8;
|
||||
|
||||
function outerValue(node: ts.Node): ts.Node {
|
||||
let ref = node;
|
||||
while (ts.isParenthesizedExpression(ref.parent) || ts.isNonNullExpression(ref.parent))
|
||||
ref = ref.parent;
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** `node` evaluates to an array of rows. */
|
||||
function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boolean {
|
||||
if (depth > MAX_DEPTH) return false;
|
||||
const ref = outerValue(node);
|
||||
const parent = ref.parent;
|
||||
if (ts.isSpreadElement(parent)) {
|
||||
const literal = parent.parent;
|
||||
return (
|
||||
ts.isArrayLiteralExpression(literal) &&
|
||||
literal.elements.length === 1 &&
|
||||
rowsReadByElements(lowerer, literal, depth + 1)
|
||||
);
|
||||
}
|
||||
if (ts.isForOfStatement(parent) && parent.expression === ref)
|
||||
return forOfReadsByElements(lowerer, parent, false);
|
||||
if (ts.isVariableDeclaration(parent) && parent.initializer === ref)
|
||||
return constBindingReadsByElements(lowerer, parent, (use) =>
|
||||
rowsReadByElements(lowerer, use, depth + 1),
|
||||
);
|
||||
if (ts.isElementAccessExpression(parent) && parent.expression === ref)
|
||||
return numberKeyRead(lowerer, parent) && rowReadByElements(lowerer, parent, depth + 1);
|
||||
if (!ts.isPropertyAccessExpression(parent) || parent.expression !== ref) return false;
|
||||
if (parent.questionDotToken !== undefined || !lowerer.isStdlibMember(parent)) return false;
|
||||
const method = parent.name.text;
|
||||
const call = parent.parent;
|
||||
if (!ts.isCallExpression(call) || call.expression !== parent)
|
||||
return method === "length" && isValueRead(parent);
|
||||
if (ROW_CALLBACK_METHODS.has(method)) {
|
||||
if (call.arguments.length !== 1 || !callbackReadsByElements(lowerer, call.arguments[0]!, 2))
|
||||
return false;
|
||||
} else if (ROW_SUBSET_METHODS.has(method) || ROW_PICK_METHODS.has(method)) {
|
||||
if (call.arguments.some((a) => ts.isSpreadElement(a))) return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (ROW_SUBSET_METHODS.has(method)) return rowsReadByElements(lowerer, call, depth + 1);
|
||||
if (ROW_PICK_METHODS.has(method)) return rowReadByElements(lowerer, call, depth + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** `node` evaluates to one row. */
|
||||
function rowReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boolean {
|
||||
if (depth > MAX_DEPTH) return false;
|
||||
const ref = outerValue(node);
|
||||
const parent = ref.parent;
|
||||
if (ts.isExpressionStatement(parent)) return true;
|
||||
if (ts.isElementAccessExpression(parent) && parent.expression === ref)
|
||||
return numberKeyRead(lowerer, parent);
|
||||
// `1 in m`: a number key tests an element.
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.operatorToken.kind === ts.SyntaxKind.InKeyword &&
|
||||
parent.right === ref
|
||||
)
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(parent.left))?.kind === "f64";
|
||||
if (ts.isPropertyAccessExpression(parent) && parent.expression === ref) {
|
||||
if (parent.questionDotToken !== undefined || !isValueRead(parent)) return false;
|
||||
if (parent.name.text === "length") return true;
|
||||
return parent.name.text === "groups" && groupsProjected(lowerer, ref as ts.Expression);
|
||||
}
|
||||
if (ts.isVariableDeclaration(parent) && parent.initializer === ref) {
|
||||
if (ts.isObjectBindingPattern(parent.name))
|
||||
return groupsPattern(parent.name) && groupsProjected(lowerer, ref as ts.Expression);
|
||||
return constBindingReadsByElements(lowerer, parent, (use) =>
|
||||
rowReadByElements(lowerer, use, depth + 1),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A for-of loop over rows: its binding is read by elements, or by its
|
||||
* companion `.index` when the loop walks the drain itself. */
|
||||
function forOfReadsByElements(
|
||||
lowerer: Lowerer,
|
||||
loop: ts.ForOfStatement,
|
||||
companion: boolean,
|
||||
): boolean {
|
||||
const list = loop.initializer;
|
||||
if (!ts.isVariableDeclarationList(list) || list.declarations.length !== 1) return false;
|
||||
const decl = list.declarations[0]!;
|
||||
if (!ts.isIdentifier(decl.name)) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(decl.name);
|
||||
if (!symbol || !bindingNeverReassigned(lowerer, symbol, decl)) return false;
|
||||
return usesOf(lowerer, decl.name, symbol).every((use) => {
|
||||
const parent = outerValue(use).parent;
|
||||
if (
|
||||
companion &&
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.name.text === "index" &&
|
||||
parent.questionDotToken === undefined &&
|
||||
isValueRead(parent)
|
||||
)
|
||||
return true;
|
||||
return rowReadByElements(lowerer, use, 1);
|
||||
});
|
||||
}
|
||||
|
||||
/** An inline callback whose first parameter (a row) is read by elements.
|
||||
* The callback may name at most `maxParams` parameters, which keeps the
|
||||
* array argument (the rows themselves) out of reach. */
|
||||
function callbackReadsByElements(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
maxParams: number,
|
||||
): boolean {
|
||||
let fn: ts.Node = node;
|
||||
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
||||
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false;
|
||||
if (fn.parameters.length > maxParams) return false;
|
||||
const param = fn.parameters[0];
|
||||
if (param === undefined) return true;
|
||||
if (param.dotDotDotToken !== undefined || param.initializer !== undefined) return false;
|
||||
if (ts.isObjectBindingPattern(param.name))
|
||||
return groupsPattern(param.name) && groupsProjectedParam(lowerer, param);
|
||||
if (!ts.isIdentifier(param.name)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!symbol || !bindingNeverReassigned(lowerer, symbol, param)) return false;
|
||||
return usesOf(lowerer, param.name, symbol).every((use) => rowReadByElements(lowerer, use, 1));
|
||||
}
|
||||
|
||||
/** Every use of a non-exported const binding satisfies `read`. */
|
||||
function constBindingReadsByElements(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
read: (use: ts.Identifier) => boolean,
|
||||
): boolean {
|
||||
if (
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.type !== undefined ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return false;
|
||||
const statement = decl.parent.parent;
|
||||
if (
|
||||
ts.isVariableStatement(statement) &&
|
||||
statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
||||
)
|
||||
return false;
|
||||
const symbol = lowerer.resolveValueSymbol(decl.name);
|
||||
if (!symbol) return false;
|
||||
return usesOf(lowerer, decl.name, symbol).every(read);
|
||||
}
|
||||
|
||||
function usesOf(lowerer: Lowerer, name: ts.Identifier, symbol: ts.Symbol): ts.Identifier[] {
|
||||
return identifiersNamed(lowerer, name.getSourceFile(), name.text).filter(
|
||||
(use) => use !== name && lowerer.resolveValueSymbol(use) === symbol,
|
||||
);
|
||||
}
|
||||
|
||||
/** `rows[i]` or `rows?.[i]` with a number key, read and not written. */
|
||||
function numberKeyRead(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean {
|
||||
if (!isValueRead(access)) return false;
|
||||
const key = lowerer.mapTypeOf(lowerer.typeOf(access.argumentExpression));
|
||||
return key?.kind === "f64";
|
||||
}
|
||||
|
||||
/** `m.groups` lowers to the compile-time projection over the row's
|
||||
* elements (lowerMatchGroupsRead). */
|
||||
function groupsProjected(lowerer: Lowerer, row: ts.Expression): boolean {
|
||||
const groups = matchResultNamedGroupsOf(lowerer, row);
|
||||
return groups !== null && groups.length > 0;
|
||||
}
|
||||
|
||||
/** `({ groups }) => ...` lowers through the same projection. */
|
||||
function groupsProjectedParam(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean {
|
||||
const groups = matchCallbackParamGroupsOf(lowerer, param);
|
||||
return groups !== null && groups.length > 0;
|
||||
}
|
||||
|
||||
/** An object pattern that binds only `groups`. */
|
||||
function groupsPattern(pattern: ts.ObjectBindingPattern): boolean {
|
||||
return (
|
||||
pattern.elements.length > 0 &&
|
||||
pattern.elements.every((el) => {
|
||||
if (el.dotDotDotToken !== undefined) return false;
|
||||
const prop = el.propertyName;
|
||||
if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups";
|
||||
return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** A read whose value is only consumed: not an assignment, update or
|
||||
* delete target. */
|
||||
function isValueRead(access: ts.Expression): 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)
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { matchAllRowsReadByElements } from "./match-row-uses.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
@@ -869,7 +870,7 @@ export function lowerRegexMethodCall(
|
||||
const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" });
|
||||
return {
|
||||
kind: "regexIntrinsic",
|
||||
method: "matchAll",
|
||||
method: matchAllRowsReadByElements(lowerer, call, false) ? "matchAllCaptures" : "matchAll",
|
||||
receiver,
|
||||
args: [re],
|
||||
type: arrayOf(arrayOf(STRING)),
|
||||
|
||||
@@ -2053,13 +2053,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
(lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile()))
|
||||
) {
|
||||
const recv = represented;
|
||||
const read: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(recv, DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc };
|
||||
// A plain slice answers from its match source without a dynamic copy.
|
||||
const read: IrExpr = typeEquals(recv.type, arrayOf(STRING))
|
||||
? { kind: "libCall", fn: "regex.resultProp", args: [recv, key], type: DYN, loc }
|
||||
: { kind: "dynKeyGet", value: lowerer.coerceToExpected(recv, DYN), key, type: DYN, loc };
|
||||
return lowerer.maybeNarrow(read, expr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
matchAllRowsReadByElements,
|
||||
storedMatchAllRowsReadByElements,
|
||||
} from "./containers/match-row-uses.js";
|
||||
import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
|
||||
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
@@ -1746,7 +1750,9 @@ function lowerMatchAllDrainDecl(
|
||||
localId: local.id,
|
||||
init: {
|
||||
kind: "regexIntrinsic",
|
||||
method: "matchAllInto",
|
||||
method: storedMatchAllRowsReadByElements(lowerer, decl)
|
||||
? "matchAllCapturesInto"
|
||||
: "matchAllInto",
|
||||
receiver,
|
||||
args: [re, { kind: "varRef", localId: idxs.id, type: idxsT, loc }],
|
||||
type: rowsT,
|
||||
@@ -9289,7 +9295,9 @@ function lowerForOfMatchAll(
|
||||
});
|
||||
rowsInit = {
|
||||
kind: "regexIntrinsic",
|
||||
method: "matchAllInto",
|
||||
method: matchAllRowsReadByElements(lowerer, call, true)
|
||||
? "matchAllCapturesInto"
|
||||
: "matchAllInto",
|
||||
receiver,
|
||||
args: [re, { kind: "varRef", localId: idxs.id, type: idxsT, loc }],
|
||||
type: rowsT,
|
||||
|
||||
@@ -3377,6 +3377,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL },
|
||||
"regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 },
|
||||
"regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 },
|
||||
// `.index` / `.input` (args[1]) of a match result. Borrowed; never throws.
|
||||
"regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN },
|
||||
// node:events EventEmitter: receivers are emitter-hierarchy objects and
|
||||
// the chaining forms (on/off/removeAll/setMax) return the receiver's
|
||||
// own class — program-dependent object types, checked in the libCall
|
||||
|
||||
@@ -53,11 +53,17 @@ export const REGEX_INTRINSIC_SIGS: Record<
|
||||
matchGroups: { receiver: STRING, argTypes: [REGEX, F64, F64], result: VOID },
|
||||
lastIndex: { receiver: REGEX, argTypes: [], result: F64 },
|
||||
matchAll: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) },
|
||||
matchAllCaptures: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) },
|
||||
matchAllInto: {
|
||||
receiver: STRING,
|
||||
argTypes: [REGEX, arrayOf(F64)],
|
||||
result: arrayOf(arrayOf(STRING)),
|
||||
},
|
||||
matchAllCapturesInto: {
|
||||
receiver: STRING,
|
||||
argTypes: [REGEX, arrayOf(F64)],
|
||||
result: arrayOf(arrayOf(STRING)),
|
||||
},
|
||||
search: { receiver: STRING, argTypes: [REGEX], result: F64 },
|
||||
source: { receiver: REGEX, argTypes: [], result: STRING },
|
||||
flags: { receiver: REGEX, argTypes: [], result: STRING },
|
||||
|
||||
@@ -2365,13 +2365,21 @@ export type IrRegexIntrinsicMethod =
|
||||
* unobservable across the lowered surface (strings are immutable; the
|
||||
* spec clones the regex at the call, so lastIndex games can't reach the
|
||||
* drain either). Non-global regexes THROW Node's exact TypeError
|
||||
* (catchable — replaceAll's stance). */
|
||||
* (catchable — replaceAll's stance). Each row carries its index, input
|
||||
* and groups properties. */
|
||||
| "matchAll"
|
||||
/** matchAll whose rows are read only through their elements
|
||||
* (matchAllRowsReadByElements): the rows carry no index, input or
|
||||
* groups properties. Same result and throw contract as matchAll. */
|
||||
| "matchAllCaptures"
|
||||
/** matchAll's companion-index form (the for-of-over-matchAll desugar):
|
||||
* args[1] is a number[] the drain ALSO fills with each match's UTF-16
|
||||
* start index — the row's `.index`, always present (every drained row
|
||||
* matched). Same result and throw contract as matchAll. */
|
||||
| "matchAllInto"
|
||||
/** matchAllInto whose rows are read only through their elements and the
|
||||
* companion index: the rows carry no index, input or groups properties. */
|
||||
| "matchAllCapturesInto"
|
||||
/** `s.search(re)` — the first match's UTF-16 index, or -1. Symbol.search
|
||||
* neither reads nor writes lastIndex (a fresh exec from position 0), so
|
||||
* no g/y fence applies: /g is irrelevant and /y anchors at 0 — exactly
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
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-match-rows-"));
|
||||
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");
|
||||
}
|
||||
|
||||
const calls = (llvm: string, symbol: string): number =>
|
||||
llvm.split("\n").filter((line) => line.includes("call ") && line.includes(`@${symbol}(`)).length;
|
||||
|
||||
test("rows read only through their elements carry no match metadata", async () => {
|
||||
const llvm = await emit(`
|
||||
const text = "k1=v1; k2=v2;";
|
||||
const re = /(?<key>k\\d)=(?<val>v\\d);/g;
|
||||
for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val, 1 in m);
|
||||
console.log([...text.matchAll(re)].map((m) => m[2]).join(","));
|
||||
console.log([...text.matchAll(re)].filter((m) => m.length > 2).find((m) => m[1] === "k2")?.[2]);
|
||||
console.log(Array.from(text.matchAll(re), ({ groups }) => groups!.key).join(","));
|
||||
const rows = [...text.matchAll(re)];
|
||||
for (const row of rows.slice(1)) console.log(row[0]);
|
||||
`);
|
||||
expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(1);
|
||||
expect(calls(llvm, "scr_regex_match_all_captures")).toBe(4);
|
||||
expect(calls(llvm, "scr_regex_match_all")).toBe(0);
|
||||
expect(calls(llvm, "scr_regex_match_all_into")).toBe(0);
|
||||
});
|
||||
|
||||
test("rows whose properties are read or that escape keep match metadata", async () => {
|
||||
const llvm = await emit(`
|
||||
const text = "k1=v1; k2=v2;";
|
||||
const re = /(?<key>k\\d)=(?<val>v\\d);/g;
|
||||
for (const m of text.matchAll(re)) console.log(m.input.length);
|
||||
console.log([...text.matchAll(re)].map((m) => m.index).join(","));
|
||||
function first(m: RegExpExecArray): string {
|
||||
return m[1]!;
|
||||
}
|
||||
console.log([...text.matchAll(re)].map(first).join(","));
|
||||
console.log([...text.matchAll(re)].map((m) => m)[0]![1]);
|
||||
`);
|
||||
expect(calls(llvm, "scr_regex_match_all_into")).toBe(1);
|
||||
expect(calls(llvm, "scr_regex_match_all")).toBe(3);
|
||||
expect(calls(llvm, "scr_regex_match_all_captures")).toBe(0);
|
||||
expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(0);
|
||||
});
|
||||
|
||||
test(".index and .input on a match slice read its match source", async () => {
|
||||
const llvm = await emit(`
|
||||
const rows = [..."a1b2".matchAll(/[a-z](\\d)/g)];
|
||||
console.log(rows[1]!.index, rows[0]!.input);
|
||||
const m = /b/.exec("ab")!;
|
||||
console.log(m.index);
|
||||
`);
|
||||
expect(calls(llvm, "scr_regex_result_prop")).toBe(3);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6010-match-metadata.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6011-match-metadata-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -40,6 +40,8 @@ static void scr_arr_oom(void) {
|
||||
/* Readers of the out-of-line storage see an all-empty one until the first
|
||||
* sparse entry, noncanonical property or metadata allocates it. */
|
||||
static const ScrArrExt scr_arr_no_ext;
|
||||
|
||||
SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks = NULL;
|
||||
#define SCR_AX(a) ((const ScrArrExt *)((a)->ext ? (a)->ext : &scr_arr_no_ext))
|
||||
|
||||
ScrArrExt *scr_arr_ext(ScrArr *a) {
|
||||
@@ -1068,6 +1070,7 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) {
|
||||
* arrays have none. */
|
||||
static void scr_arr_ext_free(ScrArr *a) {
|
||||
ScrArrExt *x = a->ext;
|
||||
if (x->match_source) scr_arr_match_hooks->release(x->match_source);
|
||||
scr_arr_sparse_free(x->sparse);
|
||||
for (size_t i = 0; i < x->prop_len; i++) free(x->props[i].key);
|
||||
free(x->props);
|
||||
@@ -4423,6 +4426,7 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) {
|
||||
}
|
||||
|
||||
void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
|
||||
if (SCR_AX(a)->match_source && scr_arr_match_hooks->copy_metadata(a, target)) return;
|
||||
if (!SCR_AX(a)->metadata) return;
|
||||
for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) {
|
||||
ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i];
|
||||
|
||||
@@ -106,6 +106,11 @@ typedef struct ScrArrExt {
|
||||
size_t prop_len;
|
||||
size_t prop_cap;
|
||||
struct ScrDyn *metadata; /* heterogeneous named properties of match results */
|
||||
/* A match result's metadata before its first use: the match's shared
|
||||
* subject and regex (scr_regex.c builds `metadata` from them) and its
|
||||
* UTF-16 start index. */
|
||||
struct ScrRegexMatchSource *match_source;
|
||||
double match_index;
|
||||
} ScrArrExt;
|
||||
/* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read
|
||||
* `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like
|
||||
@@ -207,6 +212,15 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) {
|
||||
* the candidate-root buffer like every headered type. */
|
||||
void scr_arr_release(ScrArr *a);
|
||||
void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target);
|
||||
/* The match-source operations, installed by scr_regex.c when it records a
|
||||
* pending match source (ScrArrExt.match_source); this unit links without
|
||||
* the regex runtime. copy_metadata writes the pending metadata into the
|
||||
* dynamic copy or materializes it, answering whether it wrote. */
|
||||
typedef struct ScrArrMatchHooks {
|
||||
void (*release)(struct ScrRegexMatchSource *src);
|
||||
bool (*copy_metadata)(ScrArr *a, struct ScrDyn *target);
|
||||
} ScrArrMatchHooks;
|
||||
extern SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks;
|
||||
/* The array's out-of-line storage, allocated (zeroed) on first use. */
|
||||
ScrArrExt *scr_arr_ext(ScrArr *a);
|
||||
/* Moves inline dense slots (kept in the header block of a small array) to
|
||||
|
||||
@@ -862,6 +862,9 @@ ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity
|
||||
* non-object kinds throw Node's catchable TypeErrors (strict-mode
|
||||
* wording). All three operands BORROWED (the value is retained in). */
|
||||
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value);
|
||||
/* A named (non-index) property of an extensible, unfrozen ARR, stored as
|
||||
* scr_dyn_key_set stores it. Takes ownership of value. */
|
||||
void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value);
|
||||
void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value);
|
||||
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict);
|
||||
ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */
|
||||
|
||||
@@ -7059,6 +7059,14 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con
|
||||
return;
|
||||
}
|
||||
|
||||
void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value) {
|
||||
if (!arr->v.arr.properties) {
|
||||
scr_dyn_region_store(arr, NULL); /* a table that later holds anything */
|
||||
arr->v.arr.properties = scr_dyn_new_obj();
|
||||
}
|
||||
scr_dyn_obj_set(arr->v.arr.properties, key, key_len, value);
|
||||
}
|
||||
|
||||
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
|
||||
if (scr_dyn_class_reflection_fence(recv)) return;
|
||||
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; }
|
||||
|
||||
@@ -188,7 +188,7 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) {
|
||||
|
||||
void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) {
|
||||
ScrArr *a = v;
|
||||
if (a->ext && (a->ext->props || a->ext->metadata)) {
|
||||
if (a->ext && (a->ext->props || a->ext->metadata || a->ext->match_source)) {
|
||||
scr_pub_refuse(ctx, "Cannot publish an array with non-index properties");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -610,6 +610,124 @@ static ScrStr *scr_regex_span(const ScrRegexInput *input, int start, int end) {
|
||||
return scr_str_new((const char *)input->data + start, (size_t)(end - start));
|
||||
}
|
||||
|
||||
/* ── match metadata ───────────────────────────────────────────────────── */
|
||||
|
||||
/* Results of one exec or matchAll call share their subject and regex.
|
||||
* Each result records the source and its start index; the metadata
|
||||
* object (index, input, groups) is built when a result is first
|
||||
* converted to a dynamic value, so results read only through their
|
||||
* elements never allocate it. */
|
||||
typedef struct ScrRegexMatchSource {
|
||||
size_t rc;
|
||||
ScrStr *input;
|
||||
ScrRegex *re;
|
||||
ScrDyn *input_value; /* `input` as a dynamic value, built on first use */
|
||||
} ScrRegexMatchSource;
|
||||
|
||||
static void scr_regex_materialize_metadata(ScrArr *a);
|
||||
static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target);
|
||||
|
||||
static ScrRegexMatchSource *scr_regex_source_new(ScrStr *s, ScrRegex *re) {
|
||||
ScrRegexMatchSource *src = malloc(sizeof *src);
|
||||
if (!src) scr_regex_oom();
|
||||
src->rc = 1;
|
||||
src->input = scr_str_retain(s);
|
||||
src->re = scr_regex_retain(re);
|
||||
src->input_value = NULL;
|
||||
return src;
|
||||
}
|
||||
|
||||
static void scr_regex_source_release(ScrRegexMatchSource *src) {
|
||||
if (!src || --src->rc != 0) return;
|
||||
scr_str_release(src->input);
|
||||
scr_regex_release(src->re);
|
||||
scr_dyn_release(src->input_value);
|
||||
free(src);
|
||||
}
|
||||
|
||||
/* Records the match at UTF-16 `index` of `src` as the row's pending
|
||||
* metadata. */
|
||||
static void scr_regex_defer_metadata(ScrArr *row, ScrRegexMatchSource *src, int index) {
|
||||
static const ScrArrMatchHooks hooks = { scr_regex_source_release, scr_regex_copy_pending_metadata };
|
||||
scr_arr_match_hooks = &hooks;
|
||||
ScrArrExt *x = scr_arr_ext(row);
|
||||
src->rc++;
|
||||
x->match_source = src;
|
||||
x->match_index = (double)index;
|
||||
}
|
||||
|
||||
/* The subject as a dynamic value, shared by the source's results. +1. */
|
||||
static ScrDyn *scr_regex_source_input(ScrRegexMatchSource *src) {
|
||||
if (!src->input_value) src->input_value = scr_dyn_new_str(src->input);
|
||||
return scr_dyn_retain(src->input_value);
|
||||
}
|
||||
|
||||
ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) {
|
||||
ScrArrExt *x = a->ext;
|
||||
if (x && x->match_source && !x->metadata) {
|
||||
if (key->len == 5 && memcmp(key->data, "index", 5) == 0)
|
||||
return scr_dyn_new_num(x->match_index);
|
||||
if (key->len == 5 && memcmp(key->data, "input", 5) == 0)
|
||||
return scr_regex_source_input(x->match_source);
|
||||
scr_regex_materialize_metadata(a);
|
||||
}
|
||||
ScrDyn *value = x && x->metadata ? scr_dyn_obj_get(x->metadata, key->data, key->len) : NULL;
|
||||
return scr_dyn_retain(value ? value : scr_dyn_undefined());
|
||||
}
|
||||
|
||||
/* groups is a null-prototype object mapping each group name to its first
|
||||
* participating capture (the row's own string, shared) or undefined, and
|
||||
* undefined itself for a pattern without named groups. The row holds each
|
||||
* capture as a string or a present undefined. +1. */
|
||||
static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) {
|
||||
const char *groupnames = lre_get_groupnames(bc);
|
||||
if (!groupnames) return scr_dyn_retain(scr_dyn_undefined());
|
||||
int capture_count = lre_get_capture_count(bc);
|
||||
ScrDyn *groups = scr_dyn_new_obj_null_proto();
|
||||
const char *name = groupnames;
|
||||
for (int k = 1; k < capture_count; k++) {
|
||||
size_t length = strlen(name);
|
||||
bool matched = (size_t)k < a->len && (size_t)k < a->cap && a->present[k] == SCR_ARR_VALUE;
|
||||
/* ES2025 duplicates sit in distinct alternatives: at most one
|
||||
* participates, and it wins over an earlier undefined. */
|
||||
if (length && (matched || !scr_dyn_obj_get(groups, name, length)))
|
||||
scr_dyn_obj_set(groups, name, length,
|
||||
matched ? scr_dyn_new_str((ScrStr *)(uintptr_t)a->data[k])
|
||||
: scr_dyn_retain(scr_dyn_undefined()));
|
||||
name += length + LRE_GROUP_NAME_TRAILER_LEN;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
static void scr_regex_materialize_metadata(ScrArr *a) {
|
||||
ScrArrExt *x = scr_arr_ext(a);
|
||||
ScrRegexMatchSource *src = x->match_source;
|
||||
if (!src) return;
|
||||
x->match_source = NULL;
|
||||
if (!x->metadata) {
|
||||
ScrDyn *props = scr_dyn_new_obj();
|
||||
x->metadata = props;
|
||||
scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index));
|
||||
scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src));
|
||||
scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re)));
|
||||
}
|
||||
scr_regex_source_release(src);
|
||||
}
|
||||
|
||||
static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target) {
|
||||
ScrArrExt *x = scr_arr_ext(a);
|
||||
ScrRegexMatchSource *src = x->match_source;
|
||||
if (target->kind != SCR_DYN_ARR || target->v.arr.properties || target->non_extensible ||
|
||||
target->v.arr.frozen) {
|
||||
scr_regex_materialize_metadata(a);
|
||||
return false;
|
||||
}
|
||||
scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(x->match_index));
|
||||
scr_dyn_arr_named_put(target, "input", 5, scr_regex_source_input(src));
|
||||
scr_dyn_arr_named_put(target, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re)));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ECMA-262 AdvanceStringIndex: past a whole surrogate pair under /u, one
|
||||
* code unit otherwise. ASCII cannot contain a surrogate pair. */
|
||||
static int scr_advance(const ScrRegexInput *input, int i, bool unicode) {
|
||||
@@ -713,12 +831,6 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool
|
||||
int count = lre_get_capture_count(bc);
|
||||
bool spans = lazy != 0 && !input.wide && (count >= 64 || ((mask | lazy) >> count) == 0);
|
||||
out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans);
|
||||
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));
|
||||
}
|
||||
/* A fresh dense array: every slot is written directly, then the
|
||||
* hole-free prefix is the run of materialized captures from 0. Its
|
||||
* storage pointers are fixed (the inline block never moves here), so
|
||||
@@ -750,6 +862,11 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool
|
||||
while (filled < n && present[filled] == SCR_ARR_VALUE) filled++;
|
||||
out->filled = (uint32_t)filled;
|
||||
out->len = n;
|
||||
if (metadata) {
|
||||
ScrRegexMatchSource *src = scr_regex_source_new(s, re);
|
||||
scr_regex_defer_metadata(out, src, scr_capture_index(&input, capture[0]));
|
||||
scr_regex_source_release(src);
|
||||
}
|
||||
}
|
||||
scr_regex_input_dispose(&input);
|
||||
return out; /* +1, or NULL (no match) */
|
||||
@@ -883,8 +1000,11 @@ double scr_regex_search(ScrStr *s, ScrRegex *re) {
|
||||
* Node's exact TypeError (catchable — replaceAll's stance). Empty matches
|
||||
* advance one unit, unicode-aware (AdvanceStringIndex). `indices`, when
|
||||
* non-NULL, receives each match's UTF-16 start index (the row's .index —
|
||||
* the companion array the for-of-over-matchAll desugar reads). */
|
||||
static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices) {
|
||||
* the companion array the for-of-over-matchAll desugar reads). With
|
||||
* `metadata`, each row records the match source for its index, input and
|
||||
* groups properties. */
|
||||
static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices,
|
||||
bool metadata) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
int re_flags = lre_get_flags(bc);
|
||||
if (!(re_flags & LRE_FLAG_GLOBAL)) {
|
||||
@@ -895,6 +1015,7 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices
|
||||
}
|
||||
bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
|
||||
int capture_count = lre_get_capture_count(bc);
|
||||
ScrRegexMatchSource *src = metadata ? scr_regex_source_new(s, re) : NULL;
|
||||
ScrRegexInput input;
|
||||
scr_regex_input_init(&input, re, bc, s);
|
||||
int len = input.len;
|
||||
@@ -906,30 +1027,51 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices
|
||||
if (scr_exec(&input, bc, pos) != 1) break;
|
||||
int start = scr_capture_index(&input, capture[0]);
|
||||
int end = scr_capture_index(&input, capture[1]);
|
||||
ScrArr *row = scr_arr_new(SCR_ELEM_STR, (size_t)capture_count);
|
||||
/* One allocation per row, like an exec result: every slot is written
|
||||
* directly, then the hole-free prefix is the run of captures from 0. */
|
||||
ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count);
|
||||
uint64_t *data = row->data;
|
||||
uint8_t *present = row->present;
|
||||
for (int k = 0; k < capture_count; k++) {
|
||||
const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
|
||||
if (cs == NULL || ce == NULL) {
|
||||
scr_arr_set_undefined(row, (double)row->len);
|
||||
data[k] = 0;
|
||||
present[k] = SCR_ARR_UNDEFINED;
|
||||
} else {
|
||||
scr_arr_push_ref(
|
||||
row, scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)));
|
||||
ScrStr *text =
|
||||
scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce));
|
||||
data[k] = (uint64_t)(uintptr_t)text;
|
||||
present[k] = SCR_ARR_VALUE;
|
||||
}
|
||||
}
|
||||
size_t filled = 0;
|
||||
while (filled < (size_t)capture_count && present[filled] == SCR_ARR_VALUE) filled++;
|
||||
row->filled = (uint32_t)filled;
|
||||
row->len = (size_t)capture_count;
|
||||
if (src) scr_regex_defer_metadata(row, src, start);
|
||||
scr_arr_push_ref(out, row);
|
||||
if (indices != NULL) scr_arr_push_f64(indices, (double)start);
|
||||
pos = end == start ? scr_advance(&input, end, unicode) : end;
|
||||
}
|
||||
scr_regex_source_release(src);
|
||||
scr_regex_input_dispose(&input);
|
||||
return out; /* +1 (possibly empty — Node's no-match drain is []) */
|
||||
}
|
||||
|
||||
ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re) {
|
||||
return scr_regex_match_all_core(s, re, NULL);
|
||||
return scr_regex_match_all_core(s, re, NULL, true);
|
||||
}
|
||||
|
||||
ScrArr *scr_regex_match_all_captures(ScrStr *s, ScrRegex *re) {
|
||||
return scr_regex_match_all_core(s, re, NULL, false);
|
||||
}
|
||||
|
||||
ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices) {
|
||||
return scr_regex_match_all_core(s, re, indices);
|
||||
return scr_regex_match_all_core(s, re, indices, true);
|
||||
}
|
||||
|
||||
ScrArr *scr_regex_match_all_captures_into(ScrStr *s, ScrRegex *re, ScrArr *indices) {
|
||||
return scr_regex_match_all_core(s, re, indices, false);
|
||||
}
|
||||
|
||||
/* ── replace / replaceAll ─────────────────────────────────────────────── */
|
||||
|
||||
@@ -88,11 +88,23 @@ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep);
|
||||
ScrArr *scr_regex_split(ScrStr *s, ScrRegex *re);
|
||||
ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit);
|
||||
/* matchAll drained eagerly: +1 string[][] of honest match slices; throws
|
||||
* Node's TypeError on a non-global regex (catchable). */
|
||||
* Node's TypeError on a non-global regex (catchable). Each row carries its
|
||||
* index, input and groups properties. */
|
||||
ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re);
|
||||
/* scr_regex_match_all for rows read only through their elements: the rows
|
||||
* carry no index, input or groups properties. */
|
||||
ScrArr *scr_regex_match_all_captures(ScrStr *s, ScrRegex *re);
|
||||
/* matchAll + the companion-index drain: also pushes each match's UTF-16
|
||||
* start index (f64) onto `indices` — the .index the for-of-over-matchAll
|
||||
* desugar reads. Same throw/result contract as scr_regex_match_all. */
|
||||
ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices);
|
||||
/* scr_regex_match_all_into for rows read only through their elements and
|
||||
* the companion index: the rows carry no index, input or groups
|
||||
* properties. */
|
||||
ScrArr *scr_regex_match_all_captures_into(ScrStr *s, ScrRegex *re, ScrArr *indices);
|
||||
/* `.index` / `.input` of a match result: its pending match source or its
|
||||
* metadata answers, without building the metadata object; undefined when
|
||||
* neither has the key. Borrows both; +1. */
|
||||
struct ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key);
|
||||
|
||||
#endif /* SCR_REGEX_H */
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// index and input on matchAll rows and exec results: for-of bindings that
|
||||
// read only captures and the index, bindings that escape, spreads,
|
||||
// callbacks, stored drains and closures.
|
||||
const text = "k1=v10; k22=v3; k3=v400;";
|
||||
const re = /(?<key>k\d+)=(?<val>v\d+);/g;
|
||||
|
||||
for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val);
|
||||
const kept: RegExpExecArray[] = [];
|
||||
for (const m of text.matchAll(re)) {
|
||||
kept.push(m);
|
||||
console.log(m.index, m.input === text);
|
||||
}
|
||||
console.log(kept.map((m) => `${m.index}:${m[0]}`).join(" "));
|
||||
|
||||
const rows = [...text.matchAll(re)];
|
||||
console.log(rows.map((m) => m.index).join(","), rows[1]!.index, rows[2]!.input.length);
|
||||
console.log(rows.filter((m) => m.index > 0).map((m) => m.groups!.key).join(","));
|
||||
console.log(rows.find((m) => m.index === 8)?.[2], rows.at(-1)!.index);
|
||||
console.log(Array.from(text.matchAll(re), (m) => m.index + m[0].length).join(","));
|
||||
|
||||
const drained = text.matchAll(/v(\d+)/g);
|
||||
for (const m of drained) console.log(m.index, m[1]);
|
||||
const stored = text.matchAll(/k(\d)/g);
|
||||
const later = (): string => [...stored].map((m) => m.index).join("|");
|
||||
console.log(later());
|
||||
|
||||
function startOf(m: RegExpExecArray): number {
|
||||
return m.index;
|
||||
}
|
||||
console.log([...text.matchAll(/=/g)].map(startOf).join(","));
|
||||
|
||||
const e = /(?<n>\d+);/.exec(text)!;
|
||||
console.log(e.index, e.input === text, e[1], e.groups!.n);
|
||||
const g = text.match(/k\d+/g)!;
|
||||
console.log(g.join(","), g.length);
|
||||
const plain = [..."aXbX".matchAll(/X/g)];
|
||||
console.log(plain.map((m) => m.index).join(","), plain.length);
|
||||
console.log([...text.matchAll(re)].map((m) => m[1]).join(","), [...text.matchAll(re)].length);
|
||||
console.log(Array.from(text.matchAll(re), (m) => m.groups!.key + m.length).join(","));
|
||||
for (const m of [...text.matchAll(re)].slice(1)) console.log(m[2], m.groups!.val);
|
||||
@@ -0,0 +1,31 @@
|
||||
// Match metadata on matchAll rows and exec results in JavaScript: index,
|
||||
// input and groups through callbacks, untyped functions, destructuring,
|
||||
// Object.keys and spread copies.
|
||||
const text = "k1=v10; k22=v3; k3=v400;";
|
||||
const re = /(?<key>k\d+)=(?<val>v\d+)(?<bang>!)?;/g;
|
||||
const rows = [...text.matchAll(re)];
|
||||
console.log(rows.map((m) => m.index).join(","), rows.map((m) => m.input.length).join(","));
|
||||
console.log(rows.map(({ index, groups }) => `${index}=${groups.key}`).join(" "));
|
||||
|
||||
function describe(m) {
|
||||
return [m.index, m.input === text, m.groups.key, m.groups.bang, Object.keys(m).join("|")];
|
||||
}
|
||||
for (const m of text.matchAll(re)) console.log(describe(m));
|
||||
console.log(rows.map((m) => describe(m)[0]).join(","));
|
||||
|
||||
const first = rows[0];
|
||||
const copy = { ...first };
|
||||
console.log(Object.keys(copy).join(","), copy.index, copy.groups.val, Object.getPrototypeOf(first.groups) === null);
|
||||
console.log(JSON.stringify(first), first.length);
|
||||
|
||||
const dup = [..."a1-b".matchAll(/(?<v>[a-z])|(?<v>\d)|-/g)];
|
||||
console.log(dup.map((m) => describe2(m)).join(" "));
|
||||
function describe2(m) {
|
||||
return `${m.index}:${m.groups.v}`;
|
||||
}
|
||||
|
||||
const pick = (m) => m && [m.index, m.groups];
|
||||
console.log(pick(/(?<y>\d)(?<z>x)?/.exec("ab3")), pick(/b/.exec("ab")), pick(/z/.exec("ab")));
|
||||
console.log(pick("a1".match(/(?<d>\d)/)), Object.keys("a1a".match(/a/g)).join(","));
|
||||
const none = [..."ab".matchAll(/[a-z]/g)];
|
||||
console.log(none.map((m) => m.groups === undefined && m.index >= 0).join(","));
|
||||
@@ -2199,6 +2199,8 @@
|
||||
"5996-rejections-between-macrotasks.cjs": 3034,
|
||||
"600-closures-basic.ts": 2349,
|
||||
"601-closures-loops.ts": 1788,
|
||||
"6010-match-metadata.ts": 3990,
|
||||
"6011-match-metadata-js.js": 3660,
|
||||
"602-closures-identity-recursion.ts": 2372,
|
||||
"6020-class-extends-function-constructor.js": 4300,
|
||||
"6021-asserted-literal-union-arm.ts": 3400,
|
||||
@@ -5236,6 +5238,8 @@
|
||||
"5996-rejections-between-macrotasks.cjs": 22172,
|
||||
"600-closures-basic.ts": 2534,
|
||||
"601-closures-loops.ts": 2297,
|
||||
"6010-match-metadata.ts": 5450,
|
||||
"6011-match-metadata-js.js": 4890,
|
||||
"602-closures-identity-recursion.ts": 2427,
|
||||
"6020-class-extends-function-constructor.js": 7500,
|
||||
"6021-asserted-literal-union-arm.ts": 7200,
|
||||
|
||||
Reference in New Issue
Block a user