poe-code 14.0.74 → 14.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/index.js +1283 -144
  2. package/dist/index.js.map +4 -4
  3. package/dist/metafile.json +1 -1
  4. package/package.json +1 -1
  5. package/packages/safe-js/dist/chunks/{chunk-YEZOXLY4.js → chunk-UZYQWFMY.js} +1233 -143
  6. package/packages/safe-js/dist/chunks/chunk-UZYQWFMY.js.map +7 -0
  7. package/packages/safe-js/dist/chunks/{chunk-2J6W3YGB.js → chunk-VD2AS4E4.js} +2 -2
  8. package/packages/safe-js/dist/cli.js +2 -2
  9. package/packages/safe-js/dist/core.js +1 -1
  10. package/packages/safe-js/dist/index.js +2 -2
  11. package/packages/safe-js/dist/index.js.map +1 -1
  12. package/packages/safe-js/dist/interp/bigint-operators.d.ts +3 -0
  13. package/packages/safe-js/dist/interp/bigint-operators.js +62 -0
  14. package/packages/safe-js/dist/interp/boxed.d.ts +2 -2
  15. package/packages/safe-js/dist/interp/boxed.js +4 -1
  16. package/packages/safe-js/dist/interp/globals/bigint.d.ts +2 -0
  17. package/packages/safe-js/dist/interp/globals/bigint.js +85 -0
  18. package/packages/safe-js/dist/interp/globals/console-json.js +57 -24
  19. package/packages/safe-js/dist/interp/globals/object-array.js +1 -1
  20. package/packages/safe-js/dist/interp/globals/regex.js +108 -2
  21. package/packages/safe-js/dist/interp/globals.d.ts +1 -0
  22. package/packages/safe-js/dist/interp/globals.js +2 -0
  23. package/packages/safe-js/dist/interp/host-bridge.js +2 -0
  24. package/packages/safe-js/dist/interp/interpreter.js +59 -12
  25. package/packages/safe-js/dist/interp/iteration.js +5 -2
  26. package/packages/safe-js/dist/interp/methods/number.js +1 -1
  27. package/packages/safe-js/dist/interp/methods/regex-split.d.ts +3 -0
  28. package/packages/safe-js/dist/interp/methods/regex-split.js +102 -0
  29. package/packages/safe-js/dist/interp/methods/regex.d.ts +1 -1
  30. package/packages/safe-js/dist/interp/methods/regex.js +16 -5
  31. package/packages/safe-js/dist/interp/methods/regexp-iterator.d.ts +5 -1
  32. package/packages/safe-js/dist/interp/methods/regexp-iterator.js +59 -7
  33. package/packages/safe-js/dist/interp/methods/string.js +9 -9
  34. package/packages/safe-js/dist/interp/object-model.d.ts +1 -0
  35. package/packages/safe-js/dist/interp/object-model.js +4 -1
  36. package/packages/safe-js/dist/interp/regex/compile-guard.js +26 -4
  37. package/packages/safe-js/dist/interp/regex/engine.d.ts +4 -0
  38. package/packages/safe-js/dist/interp/regex/engine.js +230 -30
  39. package/packages/safe-js/dist/interp/regex/parse.d.ts +42 -4
  40. package/packages/safe-js/dist/interp/regex/parse.js +429 -25
  41. package/packages/safe-js/dist/interp/regexp-iterator.d.ts +4 -2
  42. package/packages/safe-js/dist/interp/regexp-iterator.js +6 -1
  43. package/packages/safe-js/dist/interp/string-coercion.d.ts +1 -1
  44. package/packages/safe-js/dist/interp/string-coercion.js +8 -3
  45. package/packages/safe-js/dist/interp/values.d.ts +1 -1
  46. package/packages/safe-js/dist/interp/values.js +10 -3
  47. package/packages/safe-js/dist/lint/rules/AS-async-not-needed.js +2 -0
  48. package/packages/safe-js/dist/lint/rules/AS-missing-async.js +2 -0
  49. package/packages/safe-js/dist/lint/rules/known-globals.d.ts +1 -1
  50. package/packages/safe-js/dist/lint/rules/known-globals.js +1 -0
  51. package/packages/safe-js/dist/parse/parser.d.ts +6 -1
  52. package/packages/safe-js/dist/parse/parser.js +8 -0
  53. package/packages/safe-js/dist/parse/tokenizer.js +12 -4
  54. package/packages/safe-js/dist/parse.d.ts +1 -1
  55. package/packages/safe-js/dist/snapshot/bigint.d.ts +1 -0
  56. package/packages/safe-js/dist/snapshot/bigint.js +14 -0
  57. package/packages/safe-js/dist/snapshot/dump-format.js +12 -2
  58. package/packages/safe-js/dist/snapshot/replay-data.d.ts +6 -0
  59. package/packages/safe-js/dist/snapshot/replay-data.js +23 -5
  60. package/packages/safe-js/dist/snapshot/restore.js +10 -2
  61. package/packages/safe-js/dist/snapshot/serialize.d.ts +4 -1
  62. package/packages/safe-js/dist/snapshot/serialize.js +6 -1
  63. package/packages/safe-js/dist/snapshot/validation.js +27 -0
  64. package/packages/safe-js/dist/chunks/chunk-YEZOXLY4.js.map +0 -7
  65. /package/packages/safe-js/dist/chunks/{chunk-2J6W3YGB.js.map → chunk-VD2AS4E4.js.map} +0 -0
@@ -1,15 +1,23 @@
1
1
  import { isSandboxRegExpIterator, regexpIteratorState } from "../regexp-iterator.js";
2
- import { createSandboxClosure } from "../values.js";
3
- import { executeRegex, toMatchArray } from "./regex.js";
2
+ import { createSandboxClosure, isSandboxRegex } from "../values.js";
3
+ import { executeRegex, regexExec, toMatchArray } from "./regex.js";
4
+ import { getSandboxPropertyDescriptor, hasRegexPropertyOverride } from "../object-model.js";
5
+ import { readPropertyDescriptor } from "../accessors.js";
6
+ import { setSandboxProperty } from "../interpreter.js";
7
+ import { sandboxNumber, sandboxString } from "../string-coercion.js";
8
+ import { normalizeLastIndex } from "../regex/engine.js";
9
+ import { retainValues } from "../resources.js";
4
10
  export function nextRegExpIterator(iterator, budget) {
5
11
  const state = regexpIteratorState(iterator);
6
12
  budget?.visitNode();
7
13
  if (state.exhausted)
8
14
  return { value: undefined, done: true };
9
15
  const matcher = state.matcher;
16
+ if (!isSandboxRegex(matcher))
17
+ throw new TypeError("Custom RegExp iterator execution requires a sandbox context.");
10
18
  const input = state.input;
11
19
  const match = executeRegex(matcher, input, Number(matcher.lastIndex));
12
- if (match === null || !matcher.flags.includes("g")) {
20
+ if (match === null || !(state.global ?? matcher.flags.includes("g"))) {
13
21
  state.exhausted = true;
14
22
  state.matcher = undefined;
15
23
  state.input = undefined;
@@ -18,12 +26,56 @@ export function nextRegExpIterator(iterator, budget) {
18
26
  return { value: undefined, done: true };
19
27
  if (!state.exhausted && match.text.length === 0) {
20
28
  const index = Number(matcher.lastIndex);
21
- const unicode = matcher.flags.includes("u") || matcher.flags.includes("v");
29
+ const unicode = state.unicode ?? (matcher.flags.includes("u") || matcher.flags.includes("v"));
22
30
  const codePoint = input.codePointAt(index);
23
31
  matcher.lastIndex = index + (unicode && codePoint !== undefined && codePoint > 0xffff ? 2 : 1);
24
32
  }
25
- budget?.allocateArrayLength(match.captures.length + 1);
26
- return { value: toMatchArray(match, input), done: false };
33
+ return { value: toMatchArray(match, input, budget), done: false };
34
+ }
35
+ export async function nextObservableRegExpIterator(iterator, budget, context) {
36
+ const state = regexpIteratorState(iterator);
37
+ if (state.exhausted)
38
+ return { value: undefined, done: true };
39
+ const matcher = state.matcher;
40
+ if (isSandboxRegex(matcher) && !hasRegexPropertyOverride(matcher, ["exec"], budget) &&
41
+ (matcher.lastIndex === null || typeof matcher.lastIndex !== "object"))
42
+ return nextRegExpIterator(iterator, budget);
43
+ const input = state.input;
44
+ let result;
45
+ let field;
46
+ const release = retainValues(budget, () => [iterator, matcher, input, result, field]);
47
+ const read = (value, key) => {
48
+ if (context.getProperty !== undefined)
49
+ return context.getProperty(value, key);
50
+ const descriptor = getSandboxPropertyDescriptor(value, key, budget);
51
+ return descriptor === undefined ? undefined : readPropertyDescriptor(descriptor, value, context);
52
+ };
53
+ try {
54
+ budget.visitNode();
55
+ result = await regexExec(matcher, input, budget, context);
56
+ const global = state.global ?? (isSandboxRegex(matcher) && matcher.flags.includes("g"));
57
+ if (result === null || !global) {
58
+ state.exhausted = true;
59
+ state.matcher = undefined;
60
+ state.input = undefined;
61
+ return result === null ? { value: undefined, done: true } : { value: result, done: false };
62
+ }
63
+ field = await read(result, "0");
64
+ const text = await sandboxString(field, budget, context);
65
+ field = undefined;
66
+ if (text.length === 0) {
67
+ field = await read(matcher, "lastIndex");
68
+ const index = normalizeLastIndex(await sandboxNumber(field, budget, context));
69
+ field = undefined;
70
+ const unicode = state.unicode ?? (isSandboxRegex(matcher) && (matcher.flags.includes("u") || matcher.flags.includes("v")));
71
+ const point = input.codePointAt(index);
72
+ await setSandboxProperty(matcher, "lastIndex", index + (unicode && point !== undefined && point > 0xffff ? 2 : 1), budget, true, context);
73
+ }
74
+ return { value: result, done: false };
75
+ }
76
+ finally {
77
+ release();
78
+ }
27
79
  }
28
80
  export function getRegExpIteratorMember(property, budget) {
29
81
  if (property === Symbol.toStringTag)
@@ -43,7 +95,7 @@ export function getRegExpIteratorMember(property, budget) {
43
95
  const receiver = context?.thisValue;
44
96
  if (!isSandboxRegExpIterator(receiver))
45
97
  throw new TypeError("RegExp string iterator next requires a matching receiver.");
46
- return nextRegExpIterator(receiver, budget);
98
+ return context === undefined ? nextRegExpIterator(receiver, budget) : nextObservableRegExpIterator(receiver, budget, context);
47
99
  }
48
100
  });
49
101
  }
@@ -1,14 +1,14 @@
1
1
  import { types } from "node:util";
2
2
  import { invokeBuiltinClosure } from "../builtin-call.js";
3
3
  import { CompileScope } from "../regex/compile-guard.js";
4
- import { normalizeLastIndex } from "../regex/engine.js";
4
+ import { advanceStringIndex, normalizeLastIndex } from "../regex/engine.js";
5
5
  import { sandboxNumber, sandboxString } from "../string-coercion.js";
6
6
  import { retainValues } from "../resources.js";
7
7
  import { getSandboxDataProperty, getSandboxPropertyDescriptor, hasRegexPropertyOverride } from "../object-model.js";
8
8
  import { readPropertyDescriptor } from "../accessors.js";
9
9
  import { createSandboxBox } from "../boxed.js";
10
10
  import { setSandboxProperty } from "../interpreter.js";
11
- import { createSandboxClosure, createSandboxRegex, deepCopyFromSandbox, isSandboxClosure, isSandboxRegex, measureSandboxData } from "../values.js";
11
+ import { createSandboxClosure, createSandboxRegex, deepCopyFromSandbox, getSandboxRegexPattern, isSandboxClosure, isSandboxRegex, measureSandboxData } from "../values.js";
12
12
  import { executeRegex, getRegexMember, regexExec, regexFlagProperties, regexSearch, toMatchArray } from "./regex.js";
13
13
  import { restoreSandboxRegExpIterator } from "../regexp-iterator.js";
14
14
  export const stringMethodNames = new Set([
@@ -356,7 +356,7 @@ function callReplaceLikeMethod(value, methodName, args, budget, callClosure, use
356
356
  return replaceWithClosure(value, search, replacement, methodName === "replaceAll", budget, callClosure, context);
357
357
  }
358
358
  async function replaceRegex(value, regex, replacement, budget, callClosure, context) {
359
- if (hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget))
359
+ if (getSandboxRegexPattern(regex).groups !== undefined || hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget))
360
360
  return regexReplace(value, regex, replacement, budget, callClosure, context);
361
361
  if (regex.flags.includes("g"))
362
362
  regex.lastIndex = 0;
@@ -410,7 +410,7 @@ export async function regexReplace(input, regex, replacementInput, budget, callC
410
410
  input = undefined;
411
411
  replacement = isSandboxClosure(replacementInput) ? replacementInput : await sandboxString(replacementInput, budget, context);
412
412
  replacementInput = undefined;
413
- if (isSandboxRegex(regex) && !hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget)) {
413
+ if (isSandboxRegex(regex) && getSandboxRegexPattern(regex).groups === undefined && !hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget)) {
414
414
  release();
415
415
  return await replaceRegex(value, regex, replacement, budget, callClosure, context);
416
416
  }
@@ -641,7 +641,7 @@ function splitNormalized(value, separator, limit, budget, parent) {
641
641
  if (match === null)
642
642
  break;
643
643
  if (match.text.length === 0)
644
- splitter.lastIndex = match.index + 1;
644
+ splitter.lastIndex = advanceStringIndex(value, match.index, splitter.flags.includes("u") || splitter.flags.includes("v"));
645
645
  endedWithZeroWidthMatch = match.text.length === 0 && match.index === value.length;
646
646
  if (match.text.length === 0 &&
647
647
  (match.index === copiedThrough || match.index === value.length))
@@ -822,7 +822,7 @@ function callMatchLikeMethod(value, methodName, args, compilation, lastIndex) {
822
822
  return match?.index ?? -1;
823
823
  }
824
824
  if (methodName === "match" && !regex.flags.includes("g"))
825
- return toMatchArray(executeRegex(regex, value, lastIndex ?? Number(regex.lastIndex)), value);
825
+ return toMatchArray(executeRegex(regex, value, lastIndex ?? Number(regex.lastIndex)), value, compilation.owner?.budget);
826
826
  if (methodName === "match")
827
827
  regex.lastIndex = 0;
828
828
  const matcher = methodName === "matchAll"
@@ -843,7 +843,7 @@ function collectRegexMatches(regex, value, all, budget, lastIndex = 0) {
843
843
  matches.push(match);
844
844
  lastIndex = match.index + match.text.length;
845
845
  if (all && match.text.length === 0)
846
- regex.lastIndex = ++lastIndex;
846
+ regex.lastIndex = lastIndex = advanceStringIndex(value, lastIndex, regex.flags.includes("u") || regex.flags.includes("v"));
847
847
  } while (all);
848
848
  return matches;
849
849
  }
@@ -852,10 +852,10 @@ function splitString(value, separator, limit) {
852
852
  return split.call(value, separator, limit);
853
853
  }
854
854
  function asNumber(value) {
855
- return Number(value);
855
+ return +value;
856
856
  }
857
857
  function asNumberOrUndefined(value) {
858
- return value === undefined ? undefined : Number(value);
858
+ return value === undefined ? undefined : +value;
859
859
  }
860
860
  function asStringOrUndefined(value) {
861
861
  return value === undefined ? undefined : String(value);
@@ -16,6 +16,7 @@ export declare function isIntrinsicConstructor(value: object): boolean;
16
16
  export declare function releaseObjectPrototype(budget: Budget): void;
17
17
  export declare function getSandboxPrototype(value: object, budget?: Budget): object | null;
18
18
  export declare function hasExplicitSandboxPrototype(value: object): boolean;
19
+ export declare function hasNullObjectPrototype(value: object): boolean;
19
20
  export declare function getSandboxPropertyDescriptor(value: SandboxValue, key: PropertyKey, budget?: Budget): PropertyDescriptor | undefined;
20
21
  export declare function getSandboxDataProperty(value: SandboxValue, key: PropertyKey, budget?: Budget): SandboxValue;
21
22
  export declare function setSandboxPrototype(value: object, prototype: object | null, budget?: Budget): void;
@@ -180,6 +180,9 @@ export function getSandboxPrototype(value, budget) {
180
180
  export function hasExplicitSandboxPrototype(value) {
181
181
  return prototypes.has(value);
182
182
  }
183
+ export function hasNullObjectPrototype(value) {
184
+ return prototypes.get(value) === null && !Array.isArray(value) && !isSandboxClosure(value) && !isSandboxRegex(value);
185
+ }
183
186
  export function getSandboxPropertyDescriptor(value, key, budget) {
184
187
  const hostProperties = isSandboxClosure(value) ? value.properties : undefined;
185
188
  if (isSandboxClosure(value) && !isGuestClosure(value))
@@ -288,7 +291,7 @@ export function hasGuestObjectState(value) {
288
291
  return !intrinsicUnchanged();
289
292
  if (isLiveCapability(value))
290
293
  return true;
291
- if (functionProperties.has(value) || prototypes.has(value))
294
+ if (functionProperties.has(value) || (prototypes.has(value) && !hasNullObjectPrototype(value)))
292
295
  return true;
293
296
  if (isSandboxBox(value) || isSandboxDate(value))
294
297
  return false;
@@ -121,7 +121,7 @@ export class RegexCompileGuard {
121
121
  this.work(flags.length);
122
122
  }
123
123
  retain(pattern, valueUnits = 0) {
124
- const units = 10 + pattern.source.length + measureNode(pattern.body);
124
+ const units = measurePattern(pattern);
125
125
  if (this.ticket !== undefined) {
126
126
  this.ticket.owner.budget.resizeCompileTicket(this.ticket, units + valueUnits);
127
127
  }
@@ -139,7 +139,16 @@ export class RegexCompileGuard {
139
139
  }
140
140
  }
141
141
  export function regexCompiledData(pattern) {
142
- return (compiledData.get(pattern) ?? { units: 10 + pattern.source.length + measureNode(pattern.body) });
142
+ return (compiledData.get(pattern) ?? { units: measurePattern(pattern) });
143
+ }
144
+ function measurePattern(pattern) {
145
+ let units = 10 + pattern.source.length + measureNode(pattern.body);
146
+ if (pattern.groups !== undefined) {
147
+ units += 1;
148
+ for (const [name, indices] of Object.entries(pattern.groups))
149
+ units += name.length + 2 + indices.length;
150
+ }
151
+ return units;
143
152
  }
144
153
  function measureNode(node) {
145
154
  switch (node.type) {
@@ -148,18 +157,27 @@ function measureNode(node) {
148
157
  return 2;
149
158
  case "literal":
150
159
  return 3 + node.value.length;
160
+ case "namedBackreference":
161
+ return 3 + node.name.length;
151
162
  case "anchor":
152
163
  case "wordBoundary":
164
+ case "backreference":
153
165
  return 3;
154
166
  case "characterClass": {
155
167
  let usage = 5 + node.items.length;
168
+ if (node.operation !== undefined)
169
+ usage += 1 + node.operation.length;
170
+ if (node.strings !== undefined)
171
+ usage += 1;
156
172
  for (const item of node.items) {
157
173
  usage +=
158
174
  item.type === "character"
159
175
  ? 3 + item.value.length
160
176
  : item.type === "range"
161
177
  ? 4 + item.from.length + item.to.length
162
- : 4;
178
+ : item.type === "property" ? 4 + item.value.length + (item.strings === undefined ? 0 : 1)
179
+ : item.type === "set" ? 3 + measureNode({ type: "characterClass", ...item.value })
180
+ : item.type === "strings" ? 4 + item.values.reduce((total, value) => total + 1 + value.length, 0) : 4;
163
181
  }
164
182
  return usage;
165
183
  }
@@ -172,7 +190,11 @@ function measureNode(node) {
172
190
  return usage;
173
191
  }
174
192
  case "group":
175
- return 5 + measureNode(node.body);
193
+ return 5 + measureNode(node.body) + (node.name === undefined ? 0 : 1 + node.name.length) +
194
+ (node.modifiers === undefined ? 0 : 2 + Object.keys(node.modifiers).length);
195
+ case "lookahead":
196
+ case "lookbehind":
197
+ return 4 + measureNode(node.body);
176
198
  case "quantifier":
177
199
  return (Object.hasOwn(node, "max") ? 6 : 5) + measureNode(node.body);
178
200
  }
@@ -3,7 +3,11 @@ export type RegexMatch = {
3
3
  index: number;
4
4
  text: string;
5
5
  captures: (string | undefined)[];
6
+ indices?: ([number, number] | undefined)[];
7
+ groups?: Record<string, string | undefined>;
8
+ indicesGroups?: Record<string, [number, number] | undefined>;
6
9
  };
7
10
  export declare function matchRegex(pattern: RegexPattern, input: string, lastIndex?: number): RegexMatch | null;
8
11
  export declare function matchRegexFrom(pattern: RegexPattern, input: string, startIndex: number): RegexMatch | null;
9
12
  export declare function normalizeLastIndex(lastIndex: number): number;
13
+ export declare function advanceStringIndex(input: string, index: number, unicode: boolean): number;
@@ -1,14 +1,19 @@
1
1
  import { allocateRegexSteps } from "../budget.js";
2
2
  export function matchRegex(pattern, input, lastIndex = 0) {
3
- const startIndex = pattern.flags.global ? normalizeLastIndex(lastIndex) : 0;
3
+ const startIndex = pattern.flags.global || pattern.flags.sticky ? normalizeLastIndex(lastIndex) : 0;
4
4
  return matchRegexFrom(pattern, input, startIndex);
5
5
  }
6
6
  export function matchRegexFrom(pattern, input, startIndex) {
7
7
  if (startIndex > input.length) {
8
8
  return null;
9
9
  }
10
- for (let attempt = startIndex; attempt <= input.length; attempt += 1) {
11
- const context = { input, flags: pattern.flags, steps: 0 };
10
+ const unicode = pattern.flags.unicode || pattern.flags.unicodeSets;
11
+ if (unicode && startIndex > 0 && startIndex < input.length &&
12
+ input.charCodeAt(startIndex) >= 0xdc00 && input.charCodeAt(startIndex) <= 0xdfff &&
13
+ input.charCodeAt(startIndex - 1) >= 0xd800 && input.charCodeAt(startIndex - 1) <= 0xdbff)
14
+ startIndex--;
15
+ for (let attempt = startIndex; attempt <= input.length; attempt = advanceStringIndex(input, attempt, unicode)) {
16
+ const context = { input, flags: pattern.flags, groups: pattern.groups, direction: 1, work: { steps: 0 } };
12
17
  charge(context);
13
18
  const initialState = {
14
19
  position: attempt,
@@ -16,26 +21,63 @@ export function matchRegexFrom(pattern, input, startIndex) {
16
21
  };
17
22
  const result = matchNode(pattern.body, initialState, context).next();
18
23
  if (!result.done) {
19
- return toRegexMatch(input, attempt, result.value);
24
+ return toRegexMatch(input, attempt, result.value, pattern.flags.hasIndices, pattern.groups);
20
25
  }
26
+ if (pattern.flags.sticky)
27
+ break;
21
28
  }
22
29
  return null;
23
30
  }
24
31
  function* matchNode(node, state, context) {
25
32
  charge(context);
33
+ const unicode = context.flags.unicode || context.flags.unicodeSets;
34
+ const character = readCharacter(context.input, state.position, context.direction, unicode);
26
35
  switch (node.type) {
27
36
  case "empty":
28
37
  yield state;
29
38
  return;
30
39
  case "literal":
31
- if (charactersEqual(context.input[state.position], node.value, context.flags.ignoreCase)) {
32
- yield { ...state, position: state.position + 1 };
40
+ if (charactersEqual(character, node.value, context.flags.ignoreCase, unicode)) {
41
+ yield { ...state, position: state.position + context.direction * character.length };
33
42
  }
34
43
  return;
44
+ case "backreference":
45
+ case "namedBackreference": {
46
+ let capture;
47
+ if (node.type === "backreference")
48
+ capture = state.captures[node.index - 1];
49
+ else {
50
+ for (const index of context.groups?.[node.name] ?? []) {
51
+ charge(context);
52
+ if (state.captures[index - 1] !== undefined) {
53
+ capture = state.captures[index - 1];
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ if (capture === undefined) {
59
+ yield state;
60
+ return;
61
+ }
62
+ let position = state.position;
63
+ let reference = context.direction === 1 ? capture.start : capture.end;
64
+ const end = context.direction === 1 ? capture.end : capture.start;
65
+ while (reference !== end) {
66
+ charge(context);
67
+ const expected = readCharacter(context.input, reference, context.direction, unicode);
68
+ const actual = readCharacter(context.input, position, context.direction, unicode);
69
+ if (!charactersEqual(actual, expected, context.flags.ignoreCase, unicode))
70
+ return;
71
+ reference += context.direction * expected.length;
72
+ position += context.direction * actual.length;
73
+ }
74
+ yield { ...state, position };
75
+ return;
76
+ }
35
77
  case "dot":
36
- if (state.position < context.input.length &&
37
- (context.flags.dotAll || !isLineTerminator(context.input[state.position]))) {
38
- yield { ...state, position: state.position + 1 };
78
+ if (character !== undefined &&
79
+ (context.flags.dotAll || !isLineTerminator(character))) {
80
+ yield { ...state, position: state.position + context.direction * character.length };
39
81
  }
40
82
  return;
41
83
  case "anchor":
@@ -44,18 +86,25 @@ function* matchNode(node, state, context) {
44
86
  }
45
87
  return;
46
88
  case "wordBoundary": {
47
- const previousWord = state.position > 0 && isWordCharacter(context.input[state.position - 1]);
48
- const nextWord = state.position < context.input.length && isWordCharacter(context.input[state.position]);
89
+ const previousWord = state.position > 0 && matchesCharacterClassItem(context.input[state.position - 1], { type: "kind", kind: "word", negated: false }, context.flags.ignoreCase, unicode);
90
+ const nextWord = state.position < context.input.length && matchesCharacterClassItem(context.input[state.position], { type: "kind", kind: "word", negated: false }, context.flags.ignoreCase, unicode);
49
91
  if ((previousWord !== nextWord) !== node.negated) {
50
92
  yield state;
51
93
  }
52
94
  return;
53
95
  }
54
96
  case "characterClass": {
55
- const character = context.input[state.position];
97
+ if (context.flags.unicodeSets) {
98
+ const lengths = [...matchUnicodeSet(node, state.position, context)].sort((a, b) => b - a);
99
+ for (const length of lengths) {
100
+ charge(context);
101
+ yield { ...state, position: state.position + context.direction * length };
102
+ }
103
+ return;
104
+ }
56
105
  if (character !== undefined &&
57
- matchesCharacterClass(character, node.items, node.negated, context.flags.ignoreCase)) {
58
- yield { ...state, position: state.position + 1 };
106
+ matchesCharacterClass(character, node.items, node.negated, context)) {
107
+ yield { ...state, position: state.position + context.direction * character.length };
59
108
  }
60
109
  return;
61
110
  }
@@ -67,14 +116,30 @@ function* matchNode(node, state, context) {
67
116
  yield* matchNode(alternative, cloneState(state), context);
68
117
  }
69
118
  return;
119
+ case "lookahead":
120
+ case "lookbehind": {
121
+ const result = matchNode(node.body, cloneState(state), {
122
+ ...context, direction: node.type === "lookbehind" ? -1 : 1
123
+ }).next();
124
+ if (node.negated) {
125
+ if (result.done)
126
+ yield state;
127
+ }
128
+ else if (!result.done) {
129
+ yield { position: state.position, captures: result.value.captures };
130
+ }
131
+ return;
132
+ }
70
133
  case "group":
71
- for (const result of matchNode(node.body, cloneState(state), context)) {
134
+ for (const result of matchNode(node.body, cloneState(state), node.modifiers === undefined ? context : {
135
+ ...context, flags: { ...context.flags, ...node.modifiers }
136
+ })) {
72
137
  if (!node.capturing || node.index === undefined) {
73
138
  yield result;
74
139
  continue;
75
140
  }
76
141
  const captures = result.captures.slice();
77
- captures[node.index - 1] = { start: state.position, end: result.position };
142
+ captures[node.index - 1] = { start: Math.min(state.position, result.position), end: Math.max(state.position, result.position) };
78
143
  yield { position: result.position, captures };
79
144
  }
80
145
  return;
@@ -88,7 +153,8 @@ function* matchSequence(elements, index, state, context) {
88
153
  yield state;
89
154
  return;
90
155
  }
91
- for (const result of matchNode(elements[index], state, context)) {
156
+ const element = elements[context.direction === 1 ? index : elements.length - 1 - index];
157
+ for (const result of matchNode(element, state, context)) {
92
158
  yield* matchSequence(elements, index + 1, result, context);
93
159
  }
94
160
  }
@@ -129,11 +195,35 @@ function matchesAnchor(kind, position, context) {
129
195
  position < context.input.length &&
130
196
  isLineTerminator(context.input[position])));
131
197
  }
132
- function matchesCharacterClass(character, items, negated, ignoreCase) {
133
- const matched = items.some((item) => matchesCharacterClassItem(character, item, ignoreCase));
198
+ function matchesCharacterClass(character, items, negated, context) {
199
+ const matched = items.some((item) => {
200
+ if (context.flags.unicode)
201
+ charge(context);
202
+ return matchesCharacterClassItem(character, item, context.flags.ignoreCase, context.flags.unicode);
203
+ });
134
204
  return negated ? !matched : matched;
135
205
  }
136
- function matchesCharacterClassItem(character, item, ignoreCase) {
206
+ function matchesCharacterClassItem(character, item, ignoreCase, unicode, unicodeSets = false) {
207
+ if (item.type === "strings" || item.type === "set")
208
+ return false;
209
+ if (unicode) {
210
+ // Generated atoms only: the host classifies one code point, never executes a guest pattern.
211
+ const hex = (value) => `\\u{${value.codePointAt(0).toString(16)}}`;
212
+ let atom;
213
+ if (item.type === "character")
214
+ atom = hex(item.value);
215
+ else if (item.type === "range")
216
+ atom = `[${hex(item.from)}-${hex(item.to)}]`;
217
+ else if (item.type === "property")
218
+ atom = `\\${item.negated ? "P" : "p"}{${item.value}}`;
219
+ else {
220
+ const kind = item.kind === "digit" ? "d" : item.kind === "word" ? "w" : "s";
221
+ atom = `\\${item.negated ? kind.toUpperCase() : kind}`;
222
+ }
223
+ return new RegExp(`^(?:${atom})$`, (ignoreCase ? "i" : "") + (unicodeSets ? "v" : "u")).test(character);
224
+ }
225
+ if (item.type === "property")
226
+ return false;
137
227
  if (item.type === "character") {
138
228
  return charactersEqual(character, item.value, ignoreCase);
139
229
  }
@@ -147,10 +237,10 @@ function matchesCharacterClassItem(character, item, ignoreCase) {
147
237
  if (!ignoreCase) {
148
238
  return false;
149
239
  }
150
- const foldedCandidate = foldCharacter(character, true).charCodeAt(0);
151
- const foldedFrom = foldCharacter(item.from, true).charCodeAt(0);
152
- const foldedTo = foldCharacter(item.to, true).charCodeAt(0);
153
- return foldedCandidate >= foldedFrom && foldedCandidate <= foldedTo;
240
+ // Folding endpoints does not preserve range membership. Classify one UTF-16
241
+ // code unit using generated hex endpoints, never a guest pattern.
242
+ return new RegExp(`^[\\u${from.toString(16).padStart(4, "0")}-\\u${to.toString(16).padStart(4, "0")}]$`, "i")
243
+ .test(character);
154
244
  }
155
245
  const matched = item.kind === "digit"
156
246
  ? isDigit(character)
@@ -159,12 +249,107 @@ function matchesCharacterClassItem(character, item, ignoreCase) {
159
249
  : isSpaceCharacter(character);
160
250
  return item.negated ? !matched : matched;
161
251
  }
162
- function toRegexMatch(input, start, state) {
163
- return {
252
+ function matchUnicodeSet(set, position, context) {
253
+ charge(context);
254
+ let lengths = new Set();
255
+ for (let index = 0; index < set.items.length; index++) {
256
+ const next = matchUnicodeSetItem(set.items[index], position, context);
257
+ if (index === 0)
258
+ lengths = next;
259
+ else if (set.operation === undefined) {
260
+ for (const length of next) {
261
+ charge(context);
262
+ lengths.add(length);
263
+ }
264
+ }
265
+ else {
266
+ for (const length of lengths) {
267
+ charge(context);
268
+ if (set.operation === "intersection" ? !next.has(length) : next.has(length))
269
+ lengths.delete(length);
270
+ }
271
+ }
272
+ }
273
+ if (set.negated) {
274
+ const character = readCharacter(context.input, position, context.direction, true);
275
+ return new Set(character !== undefined && !lengths.has(character.length) ? [character.length] : []);
276
+ }
277
+ return lengths;
278
+ }
279
+ function matchUnicodeSetItem(item, position, context) {
280
+ charge(context);
281
+ if (item.type === "set")
282
+ return matchUnicodeSet(item.value, position, context);
283
+ const lengths = new Set();
284
+ if (item.type === "strings") {
285
+ for (const value of item.values) {
286
+ charge(context);
287
+ let reference = context.direction === 1 ? 0 : value.length;
288
+ let cursor = position;
289
+ const end = context.direction === 1 ? value.length : 0;
290
+ while (reference !== end) {
291
+ charge(context);
292
+ const expected = readCharacter(value, reference, context.direction, true);
293
+ const actual = readCharacter(context.input, cursor, context.direction, true);
294
+ if (!charactersEqual(actual, expected, context.flags.ignoreCase, true))
295
+ break;
296
+ reference += context.direction * expected.length;
297
+ cursor += context.direction * actual.length;
298
+ }
299
+ if (reference === end)
300
+ lengths.add(Math.abs(cursor - position));
301
+ }
302
+ return lengths;
303
+ }
304
+ if (item.type === "property" && item.strings) {
305
+ // A validated property atom denotes a finite Unicode string set. The host
306
+ // finds its longest member only; guest alternation/backtracking stays here.
307
+ const atom = `\\p{${item.value}}`;
308
+ const flags = (context.flags.ignoreCase ? "i" : "") + "v";
309
+ const matcher = new RegExp(context.direction === 1 ? atom : `(?<=(${atom}))`, flags + "y");
310
+ matcher.lastIndex = position;
311
+ const match = matcher.exec(context.input);
312
+ if (match === null)
313
+ return lengths;
314
+ const longest = context.direction === 1 ? match[0] : match[1];
315
+ const member = new RegExp(`^(?:${atom})$`, flags);
316
+ let consumed = 0;
317
+ while (consumed < longest.length) {
318
+ charge(context);
319
+ const cursor = context.direction === 1 ? consumed : longest.length - consumed;
320
+ consumed += readCharacter(longest, cursor, context.direction, true).length;
321
+ const candidate = context.direction === 1 ? longest.slice(0, consumed) : longest.slice(longest.length - consumed);
322
+ if (member.test(candidate))
323
+ lengths.add(consumed);
324
+ }
325
+ return lengths;
326
+ }
327
+ const character = readCharacter(context.input, position, context.direction, true);
328
+ if (character !== undefined && matchesCharacterClassItem(character, item, context.flags.ignoreCase, true, true))
329
+ lengths.add(character.length);
330
+ return lengths;
331
+ }
332
+ function toRegexMatch(input, start, state, hasIndices, groups) {
333
+ const match = {
164
334
  index: start,
165
335
  text: input.slice(start, state.position),
166
336
  captures: state.captures.map((capture) => capture === undefined ? undefined : input.slice(capture.start, capture.end))
167
337
  };
338
+ if (hasIndices) {
339
+ match.indices = [[start, state.position], ...state.captures.map((capture) => capture === undefined ? undefined : [capture.start, capture.end])];
340
+ }
341
+ if (groups !== undefined) {
342
+ match.groups = Object.create(null);
343
+ if (hasIndices)
344
+ match.indicesGroups = Object.create(null);
345
+ for (const [name, indices] of Object.entries(groups)) {
346
+ const index = indices.find(index => state.captures[index - 1] !== undefined);
347
+ match.groups[name] = index === undefined ? undefined : match.captures[index - 1];
348
+ if (match.indicesGroups !== undefined)
349
+ match.indicesGroups[name] = index === undefined ? undefined : match.indices[index];
350
+ }
351
+ }
352
+ return match;
168
353
  }
169
354
  function cloneState(state) {
170
355
  return { position: state.position, captures: state.captures.slice() };
@@ -194,13 +379,13 @@ function clearNodeCaptures(node, captures) {
194
379
  }
195
380
  return;
196
381
  }
197
- if (node.type === "quantifier") {
382
+ if (node.type === "quantifier" || node.type === "lookahead" || node.type === "lookbehind") {
198
383
  clearNodeCaptures(node.body, captures);
199
384
  }
200
385
  }
201
386
  function charge(context) {
202
- context.steps += 1;
203
- allocateRegexSteps(context.steps);
387
+ context.work.steps += 1;
388
+ allocateRegexSteps(context.work.steps);
204
389
  }
205
390
  export function normalizeLastIndex(lastIndex) {
206
391
  if (Number.isNaN(lastIndex) || lastIndex <= 0) {
@@ -208,9 +393,24 @@ export function normalizeLastIndex(lastIndex) {
208
393
  }
209
394
  return Math.min(Math.floor(lastIndex), Number.MAX_SAFE_INTEGER);
210
395
  }
211
- function charactersEqual(left, right, ignoreCase) {
396
+ function charactersEqual(left, right, ignoreCase, unicode = false) {
397
+ if (unicode && ignoreCase && left !== undefined)
398
+ return new RegExp(`^\\u{${right.codePointAt(0).toString(16)}}$`, "iu").test(left);
212
399
  return left !== undefined && foldCharacter(left, ignoreCase) === foldCharacter(right, ignoreCase);
213
400
  }
401
+ export function advanceStringIndex(input, index, unicode) {
402
+ return index + (unicode && (input.codePointAt(index) ?? 0) > 0xffff ? 2 : 1);
403
+ }
404
+ function readCharacter(input, position, direction, unicode) {
405
+ let index = direction === 1 ? position : position - 1;
406
+ if (index < 0 || index >= input.length)
407
+ return undefined;
408
+ if (unicode && direction === -1 && index > 0 &&
409
+ input.charCodeAt(index) >= 0xdc00 && input.charCodeAt(index) <= 0xdfff &&
410
+ input.charCodeAt(index - 1) >= 0xd800 && input.charCodeAt(index - 1) <= 0xdbff)
411
+ index--;
412
+ return unicode ? String.fromCodePoint(input.codePointAt(index)) : input[index];
413
+ }
214
414
  function foldCharacter(character, ignoreCase) {
215
415
  if (!ignoreCase) {
216
416
  return character;