mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Read named groups through optional chains and keep their null prototype (#1299)
This commit is contained in:
@@ -1 +1 @@
|
||||
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
|
||||
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Named groups (`groups`) read on matches of a statically known regex: from `exec` and `match`, through optional chains (`m?.groups?.name`), in a `for...of` over `matchAll`, and on the element parameter of an array-method callback over `matchAll` rows. The groups object has a null prototype, as in Node. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js";
|
||||
import { countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
import { objectEnumerationReceiver } from "../object-enumeration-receiver.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
@@ -852,50 +853,57 @@ export function lowerRecordOvfCaptureHelper(
|
||||
lift === "dyn"
|
||||
? { kind: "dynFrom", value: v, type: DYN, loc }
|
||||
: lowerer.applyWidthLift(lift, v, tIv, loc);
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: {
|
||||
kind: "recordLit",
|
||||
fields: inits.map((d) => ({
|
||||
name: d.name,
|
||||
value:
|
||||
d.kind === "direct"
|
||||
? lowerer.presenceKeepingCopy(
|
||||
sRef,
|
||||
fromId,
|
||||
d.name,
|
||||
lowerer.applyWidthLift(
|
||||
d.lift,
|
||||
{
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: fromId,
|
||||
field: d.name,
|
||||
type: d.src,
|
||||
loc,
|
||||
},
|
||||
to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
),
|
||||
const fresh: IrExpr & { kind: "recordLit" } = {
|
||||
kind: "recordLit",
|
||||
fields: inits.map((d) => ({
|
||||
name: d.name,
|
||||
value:
|
||||
d.kind === "direct"
|
||||
? lowerer.presenceKeepingCopy(
|
||||
sRef,
|
||||
fromId,
|
||||
d.name,
|
||||
lowerer.applyWidthLift(
|
||||
d.lift,
|
||||
{
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: fromId,
|
||||
field: d.name,
|
||||
type: d.src,
|
||||
loc,
|
||||
)
|
||||
: ({
|
||||
// No source property of this name: the field starts
|
||||
// absent (an overflow write below can supply it).
|
||||
kind: "fieldAbsent",
|
||||
unionId: d.unionId,
|
||||
type: to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
} satisfies IrExpr),
|
||||
})),
|
||||
type: toT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
},
|
||||
to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
)
|
||||
: ({
|
||||
// No source property of this name: the field starts
|
||||
// absent (an overflow write below can supply it).
|
||||
kind: "fieldAbsent",
|
||||
unionId: d.unionId,
|
||||
type: to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
} satisfies IrExpr),
|
||||
})),
|
||||
type: toT,
|
||||
loc,
|
||||
};
|
||||
// The source and its reshape are one object in JavaScript, so a
|
||||
// null-prototype source (a match's groups, Object.groupBy) keeps it.
|
||||
const init: IrExpr =
|
||||
nullProtoRecordShape(lowerer, fromT) && nullProtoRecordShape(lowerer, toT)
|
||||
? {
|
||||
kind: "ternary",
|
||||
cond: recordNullProtoTest(sRef, fromId, loc),
|
||||
then: { ...fresh, nullProto: true },
|
||||
else_: fresh,
|
||||
type: toT,
|
||||
loc,
|
||||
}
|
||||
: fresh;
|
||||
const body: IrStmt[] = [{ kind: "varDecl", localId: "out.0", init, loc }];
|
||||
const fieldStmts = new Map<string, IrStmt>();
|
||||
// Source declared fields in declaration order, skipping unset
|
||||
// optionals (stance 37) and the direct-initialized (consumed) names.
|
||||
|
||||
@@ -13739,6 +13739,8 @@ const MATCH_ROW_CALLBACK_METHODS = new Set([
|
||||
"filter",
|
||||
"find",
|
||||
"findLast",
|
||||
"findIndex",
|
||||
"findLastIndex",
|
||||
"some",
|
||||
"every",
|
||||
"flatMap",
|
||||
@@ -13852,7 +13854,10 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu
|
||||
* receiver isn't a match slice or the regex isn't traceable — the
|
||||
* caller's member fence (with the groups hint) names the gap. */
|
||||
function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.name.text !== "groups" || expr.questionDotToken !== undefined) return null;
|
||||
if (expr.name.text !== "groups") return null;
|
||||
// `m?.groups` re-dispatches through the optional-chain machinery, which
|
||||
// narrows the receiver to its match arm first.
|
||||
if (expr.questionDotToken !== undefined && !lowerer.chainHandled.has(expr)) return null;
|
||||
const loc = locOf(expr);
|
||||
const strArr = arrayOf(STRING);
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
|
||||
@@ -3046,8 +3046,8 @@ export function stdlibMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExp
|
||||
hint =
|
||||
"'.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a " +
|
||||
"const initialized with one (the group-name table is built at compile time) — and the " +
|
||||
"read is not an optional-chain step: narrow the match instead (if (m) { m.groups } or " +
|
||||
"m!.groups)";
|
||||
"match comes from exec, match, a for-of over matchAll, or the element parameter of an " +
|
||||
"array-method callback over matchAll rows";
|
||||
} else if (recvIr?.kind === "array" && member === "flat") {
|
||||
hint =
|
||||
"flat has no lowering (flatMap does) — flatten into an accumulator instead: " +
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5907-regex-groups-match-rows.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Named groups on matchAll rows (array-method callbacks over spread rows,
|
||||
// duplicate names), through optional chains on exec and match results
|
||||
// (including null matches), and the groups object's null prototype through
|
||||
// inspection, declared types and unions. Node.js prints the same.
|
||||
const text = "a1 b22 c333";
|
||||
const re = /(?<word>[a-z])(?<num>\d+)/g;
|
||||
const rows = [...text.matchAll(re)];
|
||||
console.log(rows.map((m) => `${m.groups!.word}=${m.groups!.num}`).join(" "));
|
||||
rows.forEach((m) => console.log("each", m.groups?.word, m[0]));
|
||||
console.log(rows.filter((m) => m.groups!.num.length > 1).length, rows.find((m) => m.groups!.word === "c")?.[0]);
|
||||
console.log([..."x9y8".matchAll(/(?<l>[a-z])|(?<d>\d)/g)].map((m) => `${m.groups!.l ?? "-"}${m.groups!.d ?? "-"}`).join(","));
|
||||
console.log(rows.some((m) => m.groups!.word === "b"), rows.every(function (m) { return m.groups!.num !== ""; }));
|
||||
for (const s of ["2024-05", "nope", "07/1999"]) {
|
||||
const m = /(?<year>\d{4})-(?<month>\d{2})|(?<month>\d{2})\/(?<year>\d{4})/.exec(s);
|
||||
console.log(s, m?.groups?.year, m?.groups?.month ?? "none", m?.groups);
|
||||
}
|
||||
const mm = "id=42".match(/id=(?<id>\d+)/);
|
||||
console.log(mm?.groups?.id, "nothing".match(/(?<z>z)/)?.groups?.z);
|
||||
const none = /(?<q>q)/.exec("abc");
|
||||
console.log(none?.groups, none?.groups?.q === undefined);
|
||||
const parts = /(?<y>\d)-(?<z>\w)/.exec("1-a")!;
|
||||
const g = parts.groups!;
|
||||
console.log(parts.groups, JSON.stringify(g), Object.keys(g), Object.getPrototypeOf(g) === null);
|
||||
try { console.log(String(g)); } catch (e) { console.log((e as Error).name, (e as Error).message); }
|
||||
const { y, z } = parts.groups!;
|
||||
console.log(y, z);
|
||||
const one = /(?<y>\d)/.exec("1");
|
||||
console.log(one?.groups);
|
||||
const maybeGroups = one?.groups;
|
||||
console.log(maybeGroups);
|
||||
console.log(one!.groups);
|
||||
const declared: { [k: string]: string } | undefined = one!.groups;
|
||||
console.log(declared);
|
||||
@@ -21,4 +21,4 @@ regex.ts:9:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard l
|
||||
| ^~~~~~~~
|
||||
10 | }
|
||||
|
||||
hint: '.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a const initialized with one (the group-name table is built at compile time) — and the read is not an optional-chain step: narrow the match instead (if (m) { m.groups } or m!.groups)
|
||||
hint: '.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a const initialized with one (the group-name table is built at compile time) — and the match comes from exec, match, a for-of over matchAll, or the element parameter of an array-method callback over matchAll rows
|
||||
@@ -1894,6 +1894,7 @@
|
||||
"5904-async-generator-completion.ts": 2950,
|
||||
"5905-array-from-async-generator.ts": 2750,
|
||||
"5906-array-from-async-generator-js.js": 2950,
|
||||
"5907-regex-groups-match-rows.ts": 3150,
|
||||
"5910-set-prototype-of-primitive-object-kinds.ts": 2719,
|
||||
"5911-dyn-class-to-primitive.ts": 2850,
|
||||
"5912-dyn-class-conversion-methods.ts": 3251,
|
||||
@@ -4602,6 +4603,7 @@
|
||||
"5904-async-generator-completion.ts": 3800,
|
||||
"5905-array-from-async-generator.ts": 3500,
|
||||
"5906-array-from-async-generator-js.js": 3750,
|
||||
"5907-regex-groups-match-rows.ts": 3750,
|
||||
"5910-set-prototype-of-primitive-object-kinds.ts": 5033,
|
||||
"5911-dyn-class-to-primitive.ts": 4732,
|
||||
"5912-dyn-class-conversion-methods.ts": 4424,
|
||||
|
||||
Reference in New Issue
Block a user