@descent-vtt/spec-graph 0.4.0 → 0.6.0

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.
package/dist/regex.js ADDED
@@ -0,0 +1,822 @@
1
+ /**
2
+ * A regular-expression matcher that cannot backtrack.
3
+ *
4
+ * `~=` is the one predicate in the selector language that used to hand a
5
+ * user-supplied string to `RegExp` and wait. That is tolerable at a prompt,
6
+ * where a human can press Ctrl-C. It is not tolerable in `.spec-graph.json`,
7
+ * where the same string runs unattended in CI against every node in the corpus.
8
+ *
9
+ * The cost is neither theoretical nor exotic. `^([A-Za-z0-9_]+[ ]?)+$` is what
10
+ * somebody writes to say "the title is words separated by single spaces", and
11
+ * against a title that does not match, V8 tries one branch per way of cutting
12
+ * each word into pieces:
13
+ *
14
+ * ```text
15
+ * RegExp here
16
+ * the quick brown fox jumps over the lazy dog! 0.9s 11us
17
+ * the quick brown fox jumps over the lazy dog and! 5.4s 12us
18
+ * the quick brown fox jumps over the lazy dog and keeps! 103s 13us
19
+ * ```
20
+ *
21
+ * Each word of k letters can be cut into pieces 2^(k-1) ways, and the engine
22
+ * tries the product of those, which is why adding one five-letter word costs
23
+ * another factor of sixteen. A sixty-character title does not finish this year.
24
+ *
25
+ * So a pattern here is compiled to a **non-deterministic finite automaton** and
26
+ * simulated over the subject one character at a time (Thompson, 1968), keeping
27
+ * a set of live states instead of a stack of choices. Each state is visited at
28
+ * most once per position, which makes the work exactly O(pattern x subject):
29
+ * the right-hand column above, growing with the subject and not with the
30
+ * pattern's ambiguity.
31
+ *
32
+ * What it costs is a dialect. Backreferences and lookaround are not regular and
33
+ * have no place in a linear automaton, so they are **rejected while compiling**,
34
+ * with a message and an offset, rather than accepted and then slow. That is the
35
+ * whole trade: a predicate that finishes, against a short list of constructs
36
+ * that a documentation selector never needed.
37
+ *
38
+ * Matching is unanchored and, by default, case-insensitive, because that is
39
+ * what `~=` has always been - `new RegExp(source, 'i')` and `.test()`. Globs
40
+ * compile to this as well, and a path on a case-sensitive filesystem needs the
41
+ * case respected, which is the one option. There are no capture groups at all:
42
+ * nothing asks where the match was, only whether there was one, which is why
43
+ * this is a plain NFA simulation rather than a Pike VM.
44
+ *
45
+ * See ADR-0017.
46
+ */
47
+ /* -------------------------------------------------------------------------- */
48
+ /* Errors */
49
+ /* -------------------------------------------------------------------------- */
50
+ /** A pattern that will not compile, and where in it the problem starts. */
51
+ export class PatternError extends Error {
52
+ /** Offset into the pattern source. */
53
+ offset;
54
+ constructor(message, offset) {
55
+ super(message);
56
+ this.name = 'PatternError';
57
+ this.offset = offset;
58
+ }
59
+ }
60
+ /**
61
+ * Ceiling on the compiled program.
62
+ *
63
+ * Counted repetition is compiled by copying - `(ab){20}` is twenty copies -
64
+ * which is part of what keeps the simulation flat, and also what makes
65
+ * `(a{99}){99}` into ten thousand states. The bound has to live somewhere, and
66
+ * a number checked while compiling is the only place it can be enforced without
67
+ * measuring anything at match time.
68
+ *
69
+ * Far above any real selector: the widest pattern in this repository's own
70
+ * tests compiles to seventeen states.
71
+ */
72
+ const MAX_PROGRAM = 4096;
73
+ /** `.` - anything but a line terminator, which is the default `RegExp` reading. */
74
+ const DOT = { negated: true, ranges: [[0x0a, 0x0a], [0x0d, 0x0d], [0x2028, 0x2029]] };
75
+ const DIGIT = [[0x30, 0x39]];
76
+ const WORD = [
77
+ [0x30, 0x39],
78
+ [0x41, 0x5a],
79
+ [0x5f, 0x5f],
80
+ [0x61, 0x7a],
81
+ ];
82
+ /** `\s`, taken from the specification rather than guessed at. */
83
+ const SPACE = [
84
+ [0x09, 0x0d],
85
+ [0x20, 0x20],
86
+ [0xa0, 0xa0],
87
+ [0x1680, 0x1680],
88
+ [0x2000, 0x200a],
89
+ [0x2028, 0x2029],
90
+ [0x202f, 0x202f],
91
+ [0x205f, 0x205f],
92
+ [0x3000, 0x3000],
93
+ [0xfeff, 0xfeff],
94
+ ];
95
+ /**
96
+ * Folds a code unit the way a case-insensitive `RegExp` does.
97
+ *
98
+ * This is `Canonicalize` from the language specification, non-Unicode mode:
99
+ * upper-case the character, keep it unchanged if that produced anything other
100
+ * than one code unit, and keep it unchanged if upper-casing dragged a
101
+ * non-ASCII character into ASCII. The last clause is the one nobody expects and
102
+ * the reason U+017F (long s) does not match `s`.
103
+ */
104
+ function canonical(code) {
105
+ // Every subject a glob sees is mostly ASCII, where the answer needs no string.
106
+ // The general path below gives the same answer here, only slower.
107
+ if (code < 128)
108
+ return code >= 97 && code <= 122 ? code - 32 : code;
109
+ const upper = String.fromCharCode(code).toUpperCase();
110
+ if (upper.length !== 1)
111
+ return code;
112
+ const folded = upper.charCodeAt(0);
113
+ if (code >= 128 && folded < 128)
114
+ return code;
115
+ return folded;
116
+ }
117
+ /**
118
+ * Whether a set accepts a code unit, case-insensitively.
119
+ *
120
+ * The specification canonicalises both sides: a character matches a set when
121
+ * *some* member of the set folds to the same thing it does. A single-character
122
+ * member can be asked that exactly. A range cannot without a fold table for
123
+ * every code unit, so a range is asked the question backwards - does the
124
+ * character, or either of its own case forms, fall inside it - which is exact
125
+ * for every character whose fold is its own case change, and therefore for
126
+ * every range anyone has written.
127
+ */
128
+ function accepts(set, code) {
129
+ const folded = canonical(code);
130
+ let hit = false;
131
+ for (let i = 0; i < set.ranges.length; i += 1) {
132
+ const [low, high] = set.ranges[i];
133
+ if (low === high ? low === code || canonical(low) === folded : inRange(low, high, code)) {
134
+ hit = true;
135
+ break;
136
+ }
137
+ }
138
+ return set.negated ? !hit : hit;
139
+ }
140
+ /**
141
+ * Whether some member of a range folds to the same thing a code unit does.
142
+ *
143
+ * Asked backwards, by trying the character's own case forms against the raw
144
+ * range - and then checking that the form found there really does fold to the
145
+ * same place, which is the part that is easy to leave out. U+017F upper-cases
146
+ * to `S`, so without that check it matches `[A-Z]`, and it must not: the fold
147
+ * keeps it outside ASCII, which is the same clause `isWordChar` gets wrong if
148
+ * nobody is watching. The differential corpus caught both.
149
+ */
150
+ function inRange(low, high, code) {
151
+ if (code >= low && code <= high)
152
+ return true;
153
+ // An ASCII character's only other case form is its ASCII letter twin, and the
154
+ // two always fold together - so the loop below would ask exactly this.
155
+ if (code < 128) {
156
+ const twin = code >= 65 && code <= 90 ? code + 32 : code >= 97 && code <= 122 ? code - 32 : -1;
157
+ return twin >= low && twin <= high;
158
+ }
159
+ const folded = canonical(code);
160
+ const char = String.fromCharCode(code);
161
+ for (const variant of [char.toLowerCase(), char.toUpperCase()]) {
162
+ if (variant.length !== 1)
163
+ continue;
164
+ const candidate = variant.charCodeAt(0);
165
+ if (candidate >= low && candidate <= high && canonical(candidate) === folded)
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+ /**
171
+ * Whether a set accepts a code unit as written.
172
+ *
173
+ * What `RegExp` does without the `i` flag, and so none of the folding above
174
+ * applies - which also means none of its one divergence does.
175
+ */
176
+ function acceptsExactly(set, code) {
177
+ return within(set.ranges, code) !== set.negated;
178
+ }
179
+ function within(ranges, code) {
180
+ for (let i = 0; i < ranges.length; i += 1) {
181
+ const range = ranges[i];
182
+ if (code >= range[0] && code <= range[1])
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+ /**
188
+ * Word characters for `\b`, which is exactly `\w` here.
189
+ *
190
+ * The specification does widen this set under case-insensitivity - to every
191
+ * character that folds into it, which is U+017F (long s) and U+212A (the
192
+ * Kelvin sign) - but only in Unicode mode, where `Canonicalize` has no clause
193
+ * keeping a non-ASCII character out of ASCII. This is the non-Unicode dialect,
194
+ * so both stay outside and the table for `\w` answers for `\b` as well.
195
+ *
196
+ * Written down because the first version of this function widened the set, and
197
+ * the differential test against `RegExp` is what said otherwise. Reading the
198
+ * clause was not enough; running it was. See ADR-0017.
199
+ */
200
+ function isWordChar(code) {
201
+ return within(WORD, code);
202
+ }
203
+ /* -------------------------------------------------------------------------- */
204
+ /* Parsing */
205
+ /* -------------------------------------------------------------------------- */
206
+ class PatternParser {
207
+ source;
208
+ position = 0;
209
+ constructor(source) {
210
+ this.source = source;
211
+ }
212
+ parse() {
213
+ const node = this.parseAlternation();
214
+ // Every character is an atom except the two that close something, and
215
+ // `parseSequence` stops on both, so anything left over is an unopened `)`.
216
+ if (this.position < this.source.length)
217
+ throw new PatternError('unmatched ")"', this.position);
218
+ return node;
219
+ }
220
+ parseAlternation() {
221
+ const options = [this.parseSequence()];
222
+ while (this.peek() === '|') {
223
+ this.position += 1;
224
+ options.push(this.parseSequence());
225
+ }
226
+ return options.length === 1 ? options[0] : { kind: 'alternate', options };
227
+ }
228
+ parseSequence() {
229
+ const parts = [];
230
+ for (;;) {
231
+ const char = this.peek();
232
+ if (char === undefined || char === '|' || char === ')')
233
+ break;
234
+ parts.push(this.parseQuantified());
235
+ }
236
+ if (parts.length === 0)
237
+ return { kind: 'empty' };
238
+ return parts.length === 1 ? parts[0] : { kind: 'concat', parts };
239
+ }
240
+ parseQuantified() {
241
+ const start = this.position;
242
+ // A group makes its contents quantifiable: `RegExp` rejects `\b*` and
243
+ // accepts `(\b)*`, which is the same nothing said twice, and this parser
244
+ // treats a group as transparent - so whether one was written is the only
245
+ // thing left that distinguishes the two.
246
+ const grouped = this.peek() === '(';
247
+ const atom = this.parseAtom();
248
+ const bounds = this.parseQuantifier();
249
+ if (bounds === null)
250
+ return atom;
251
+ if (atom.kind === 'assert' && !grouped) {
252
+ throw new PatternError('nothing to repeat: a quantifier cannot follow an anchor', start);
253
+ }
254
+ return { kind: 'repeat', node: atom, min: bounds.min, max: bounds.max };
255
+ }
256
+ /** `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`, and a trailing `?` that is ignored. */
257
+ parseQuantifier() {
258
+ const char = this.peek();
259
+ let bounds = null;
260
+ if (char === '*')
261
+ bounds = { min: 0, max: Infinity };
262
+ else if (char === '+')
263
+ bounds = { min: 1, max: Infinity };
264
+ else if (char === '?')
265
+ bounds = { min: 0, max: 1 };
266
+ if (bounds !== null)
267
+ this.position += 1;
268
+ else if (char === '{')
269
+ bounds = this.parseBraces();
270
+ if (bounds === null)
271
+ return null;
272
+ // Laziness decides *which* match is found, and nothing here asks: the
273
+ // question is whether one exists, and a lazy quantifier accepts the same
274
+ // language as a greedy one.
275
+ if (this.peek() === '?')
276
+ this.position += 1;
277
+ return bounds;
278
+ }
279
+ /**
280
+ * Reads `{n,m}`, or nothing.
281
+ *
282
+ * A `{` that does not open a counted repetition is a literal brace, which is
283
+ * what `RegExp` does with `a{` and what any pattern written against a
284
+ * document full of `{0}` placeholders depends on.
285
+ */
286
+ parseBraces() {
287
+ const match = /^\{(\d+)(?:(,)(\d*))?\}/.exec(this.source.slice(this.position));
288
+ if (!match)
289
+ return null;
290
+ const min = Number(match[1]);
291
+ const max = match[2] === undefined ? min : match[3] === '' ? Infinity : Number(match[3]);
292
+ if (min > max)
293
+ throw new PatternError('numbers out of order in a counted repetition', this.position);
294
+ this.position += match[0].length;
295
+ return { min, max };
296
+ }
297
+ parseAtom() {
298
+ const start = this.position;
299
+ const char = this.peek();
300
+ switch (char) {
301
+ case undefined:
302
+ throw new PatternError('pattern ends where an expression was expected', start);
303
+ case '(':
304
+ return this.parseGroup();
305
+ case '[':
306
+ return { kind: 'char', set: this.parseClass() };
307
+ case '.':
308
+ this.position += 1;
309
+ return { kind: 'char', set: DOT };
310
+ case '^':
311
+ this.position += 1;
312
+ return { kind: 'assert', assertion: 'start' };
313
+ case '$':
314
+ this.position += 1;
315
+ return { kind: 'assert', assertion: 'end' };
316
+ case '*':
317
+ case '+':
318
+ throw new PatternError(`nothing to repeat before "${char}"`, start);
319
+ case '\\':
320
+ return this.parseEscape();
321
+ default:
322
+ this.position += 1;
323
+ return { kind: 'char', set: literal(char.charCodeAt(0)) };
324
+ }
325
+ }
326
+ /**
327
+ * A group, which is never a capture.
328
+ *
329
+ * `(a)` and `(?:a)` compile to the same automaton because nothing downstream
330
+ * can ask what `(a)` caught. A named group is accepted and its name dropped,
331
+ * so a pattern copied out of somewhere else keeps working.
332
+ */
333
+ parseGroup() {
334
+ const start = this.position;
335
+ this.position += 1;
336
+ if (this.source.startsWith('?:', this.position)) {
337
+ this.position += 2;
338
+ }
339
+ else if (this.peek() === '?') {
340
+ const named = /^\?<([A-Za-z_$][\w$]*)>/.exec(this.source.slice(this.position));
341
+ if (named) {
342
+ this.position += named[0].length;
343
+ }
344
+ else if (/^\?(?:=|!|<=|<!)/.test(this.source.slice(this.position))) {
345
+ throw new PatternError('lookaround is not supported: an automaton that cannot backtrack cannot look ahead - use ^= $= or *= for a fixed prefix, suffix or substring', start);
346
+ }
347
+ else {
348
+ throw new PatternError('unsupported group: expected (?:...) or (?<name>...)', start);
349
+ }
350
+ }
351
+ const inner = this.parseAlternation();
352
+ if (this.peek() !== ')')
353
+ throw new PatternError('unmatched "("', start);
354
+ this.position += 1;
355
+ return inner;
356
+ }
357
+ /**
358
+ * Reads a `[...]` class.
359
+ *
360
+ * `[]` matches nothing and `[^]` matches anything, which reads like a mistake
361
+ * and is what every JavaScript engine does: the class ends at the first `]`,
362
+ * even when that `]` is the first character inside it.
363
+ */
364
+ parseClass() {
365
+ const start = this.position;
366
+ this.position += 1;
367
+ const negated = this.peek() === '^';
368
+ if (negated)
369
+ this.position += 1;
370
+ const ranges = [];
371
+ while (this.position < this.source.length && this.peek() !== ']') {
372
+ const low = this.parseClassAtom();
373
+ // A `-` between two single characters is a range. Anywhere else - at
374
+ // either end of the class, or beside a `\d`-style set, which has no
375
+ // endpoint - it is a literal hyphen, exactly as Annex B has it.
376
+ if (this.peek() !== '-' || this.source[this.position + 1] === ']' || this.position + 1 >= this.source.length) {
377
+ push(ranges, low);
378
+ continue;
379
+ }
380
+ const dash = this.position;
381
+ this.position += 1;
382
+ const high = this.parseClassAtom();
383
+ if (typeof low === 'number' && typeof high === 'number') {
384
+ if (low > high)
385
+ throw new PatternError('characters out of order in a character class', dash);
386
+ ranges.push([low, high]);
387
+ continue;
388
+ }
389
+ push(ranges, low);
390
+ ranges.push([0x2d, 0x2d]);
391
+ push(ranges, high);
392
+ }
393
+ if (this.peek() !== ']')
394
+ throw new PatternError('unterminated character class', start);
395
+ this.position += 1;
396
+ return { negated, ranges };
397
+ }
398
+ /** One member of a class: a code unit, or a whole set for `\d` and its kind. */
399
+ parseClassAtom() {
400
+ if (this.peek() !== '\\') {
401
+ const code = this.peek().charCodeAt(0);
402
+ this.position += 1;
403
+ return code;
404
+ }
405
+ // `\b` is a backspace inside a class and a word boundary outside one, which
406
+ // is the only place the two readings of an escape diverge.
407
+ if (this.source[this.position + 1] === 'b') {
408
+ this.position += 2;
409
+ return 0x08;
410
+ }
411
+ if (this.source[this.position + 1] === 'B') {
412
+ throw new PatternError('"\\B" is an assertion, not a character, so it cannot be in a class', this.position);
413
+ }
414
+ const set = this.parseCharacterEscape();
415
+ if (!set.negated && set.ranges.length === 1) {
416
+ const [low, high] = set.ranges[0];
417
+ if (low === high)
418
+ return low;
419
+ }
420
+ return set.negated ? invert(set.ranges) : set.ranges;
421
+ }
422
+ /** An escape outside a class, where `\b` and `\B` are assertions. */
423
+ parseEscape() {
424
+ const next = this.source[this.position + 1];
425
+ if (next === 'b' || next === 'B') {
426
+ this.position += 2;
427
+ return { kind: 'assert', assertion: next === 'b' ? 'word-boundary' : 'not-word-boundary' };
428
+ }
429
+ return { kind: 'char', set: this.parseCharacterEscape() };
430
+ }
431
+ /** Every escape that stands for a character or a set of them. */
432
+ parseCharacterEscape() {
433
+ const start = this.position;
434
+ this.position += 1;
435
+ const char = this.peek();
436
+ if (char === undefined)
437
+ throw new PatternError('pattern ends with a backslash', start);
438
+ this.position += 1;
439
+ switch (char) {
440
+ case 'd':
441
+ return { negated: false, ranges: DIGIT };
442
+ case 'D':
443
+ return { negated: true, ranges: DIGIT };
444
+ case 'w':
445
+ return { negated: false, ranges: WORD };
446
+ case 'W':
447
+ return { negated: true, ranges: WORD };
448
+ case 's':
449
+ return { negated: false, ranges: SPACE };
450
+ case 'S':
451
+ return { negated: true, ranges: SPACE };
452
+ case 'n':
453
+ return literal(0x0a);
454
+ case 'r':
455
+ return literal(0x0d);
456
+ case 't':
457
+ return literal(0x09);
458
+ case 'f':
459
+ return literal(0x0c);
460
+ case 'v':
461
+ return literal(0x0b);
462
+ case '0':
463
+ if (/^\d/.test(this.source.slice(this.position))) {
464
+ throw new PatternError('an octal escape is not supported - write \\xHH or \\uHHHH', start);
465
+ }
466
+ return literal(0);
467
+ case 'x':
468
+ return literal(this.readHex(2, start));
469
+ case 'u':
470
+ return literal(this.readHex(4, start));
471
+ case 'c': {
472
+ const letter = this.peek();
473
+ if (letter === undefined || !/[A-Za-z]/.test(letter)) {
474
+ throw new PatternError('expected a letter after "\\c"', start);
475
+ }
476
+ this.position += 1;
477
+ return literal(letter.charCodeAt(0) % 32);
478
+ }
479
+ case 'k':
480
+ throw new PatternError('a named backreference is not supported: a backreference is not a regular expression, and this is an automaton', start);
481
+ case 'p':
482
+ case 'P':
483
+ throw new PatternError(`"\\${char}" needs a table of Unicode properties, which this matcher does not carry - write the characters out as a [class]`, start);
484
+ default:
485
+ if (/[1-9]/.test(char)) {
486
+ throw new PatternError('a backreference is not supported: it is not a regular expression, and this is an automaton', start);
487
+ }
488
+ if (/[A-Za-z]/.test(char)) {
489
+ // `RegExp` reads an unknown letter escape as the letter itself, so
490
+ // `\A` quietly matches a capital A and `\z` a lower-case z. Saying so
491
+ // is worth more than agreeing with a rule nobody wanted.
492
+ throw new PatternError(`unknown escape "\\${char}"`, start);
493
+ }
494
+ return literal(char.charCodeAt(0));
495
+ }
496
+ }
497
+ readHex(digits, start) {
498
+ const raw = this.source.slice(this.position, this.position + digits);
499
+ if (raw.length !== digits || !/^[0-9a-fA-F]+$/.test(raw)) {
500
+ throw new PatternError(`expected ${digits} hex digits after "\\${this.source[start + 1]}"`, start);
501
+ }
502
+ this.position += digits;
503
+ return Number.parseInt(raw, 16);
504
+ }
505
+ peek() {
506
+ return this.source[this.position];
507
+ }
508
+ }
509
+ function literal(code) {
510
+ return { negated: false, ranges: [[code, code]] };
511
+ }
512
+ function push(ranges, atom) {
513
+ if (typeof atom === 'number')
514
+ ranges.push([atom, atom]);
515
+ else
516
+ ranges.push(...atom);
517
+ }
518
+ /**
519
+ * Complement of a range list, so `[\D]` can sit inside a class.
520
+ *
521
+ * A negated escape is a set with no endpoints, and a class is a union of
522
+ * ranges, so the only way to put one inside the other is to spell out what it
523
+ * leaves.
524
+ */
525
+ function invert(ranges) {
526
+ const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
527
+ const out = [];
528
+ let cursor = 0;
529
+ for (const [low, high] of sorted) {
530
+ if (low > cursor)
531
+ out.push([cursor, low - 1]);
532
+ cursor = Math.max(cursor, high + 1);
533
+ }
534
+ if (cursor <= 0xffff)
535
+ out.push([cursor, 0xffff]);
536
+ return out;
537
+ }
538
+ class Compiler {
539
+ code = [];
540
+ add(instruction) {
541
+ if (this.code.length >= MAX_PROGRAM) {
542
+ throw new PatternError(`pattern is too large: counted repetition is compiled by copying, and this needs more than ${MAX_PROGRAM} states`, 0);
543
+ }
544
+ this.code.push(instruction);
545
+ return instruction;
546
+ }
547
+ get here() {
548
+ return this.code.length;
549
+ }
550
+ emitMatch() {
551
+ this.add({ op: 'match' });
552
+ }
553
+ compile(node) {
554
+ switch (node.kind) {
555
+ case 'empty':
556
+ return;
557
+ case 'char':
558
+ this.add({ op: 'char', set: node.set });
559
+ return;
560
+ case 'assert':
561
+ this.add({ op: 'assert', assertion: node.assertion });
562
+ return;
563
+ case 'concat':
564
+ for (const part of node.parts)
565
+ this.compile(part);
566
+ return;
567
+ case 'alternate': {
568
+ // One split per option but the last. Each split falls through to its
569
+ // own branch and points its second arm at the next split; each branch
570
+ // ends in a jump past all of them.
571
+ const ends = [];
572
+ for (let i = 0; i < node.options.length - 1; i += 1) {
573
+ const split = this.add({ op: 'split', x: 0, y: 0 });
574
+ split.x = this.here;
575
+ this.compile(node.options[i]);
576
+ ends.push(this.add({ op: 'jump', to: 0 }));
577
+ split.y = this.here;
578
+ }
579
+ this.compile(node.options[node.options.length - 1]);
580
+ for (const end of ends)
581
+ end.to = this.here;
582
+ return;
583
+ }
584
+ case 'repeat':
585
+ this.repeat(node.node, node.min, node.max);
586
+ return;
587
+ }
588
+ }
589
+ repeat(node, min, max) {
590
+ for (let i = 0; i < min; i += 1)
591
+ this.compile(node);
592
+ if (max === Infinity) {
593
+ const split = this.add({ op: 'split', x: 0, y: 0 });
594
+ const body = this.here;
595
+ split.x = body;
596
+ this.compile(node);
597
+ this.add({ op: 'jump', to: body - 1 });
598
+ split.y = this.here;
599
+ return;
600
+ }
601
+ // `{2,5}` is two copies and then three optional ones, each able to leave
602
+ // for the far end rather than for the next copy.
603
+ const exits = [];
604
+ for (let i = min; i < max; i += 1) {
605
+ const split = this.add({ op: 'split', x: 0, y: 0 });
606
+ split.x = this.here;
607
+ exits.push(split);
608
+ this.compile(node);
609
+ }
610
+ for (const split of exits)
611
+ split.y = this.here;
612
+ }
613
+ }
614
+ /**
615
+ * Compiles a pattern into a matcher.
616
+ *
617
+ * Throws {@link PatternError} on anything it cannot run in linear time, which
618
+ * is the point: the alternative to a message here is a build that stops.
619
+ */
620
+ export function compilePattern(source, options = {}) {
621
+ const tree = new PatternParser(source).parse();
622
+ const compiler = new Compiler();
623
+ compiler.compile(tree);
624
+ compiler.emitMatch();
625
+ // A pattern that begins by asserting the start of the subject cannot match
626
+ // anywhere else, so the simulation stops seeding new threads at every
627
+ // position. Nothing depends on this being noticed - it is the difference
628
+ // between one pass and two over a long body.
629
+ const machine = new Machine(compiler.code, startsAnchored(tree), options.ignoreCase !== false);
630
+ return {
631
+ size: compiler.code.length,
632
+ test: (subject) => machine.test(subject),
633
+ };
634
+ }
635
+ /** True when every path through the pattern opens by asserting the start. */
636
+ function startsAnchored(node) {
637
+ switch (node.kind) {
638
+ case 'assert':
639
+ return node.assertion === 'start';
640
+ case 'concat': {
641
+ const first = node.parts[0];
642
+ return first !== undefined && startsAnchored(first);
643
+ }
644
+ case 'alternate':
645
+ return node.options.every((option) => startsAnchored(option));
646
+ case 'repeat':
647
+ return node.min > 0 && startsAnchored(node.node);
648
+ default:
649
+ return false;
650
+ }
651
+ }
652
+ const OP_CHAR = 0;
653
+ const OP_SPLIT = 1;
654
+ const OP_JUMP = 2;
655
+ const OP_ASSERT = 3;
656
+ const OP_MATCH = 4;
657
+ /** Fills the slots of states that are not characters, where nothing reads it. */
658
+ const NOTHING = { negated: false, ranges: [] };
659
+ /**
660
+ * The simulation, over the program flattened into typed arrays.
661
+ *
662
+ * The compiler's instructions are objects of five shapes, and one loop reading
663
+ * `op` off all five is a megamorphic property read on every step. Simulated that
664
+ * way, with a fresh visited table per subject, a glob cost about 3.5
665
+ * microseconds a path and a path that failed on its first character still paid
666
+ * half a microsecond for the allocation. Flat arrays and buffers kept between
667
+ * subjects bring those to 0.6 and 0.04 - `RegExp` takes 0.06 - which matters
668
+ * because a walk tests every path it visits against every pattern.
669
+ */
670
+ class Machine {
671
+ ops;
672
+ /** A split's first branch, or a jump's target. */
673
+ first;
674
+ /** A split's second branch. */
675
+ second;
676
+ sets;
677
+ assertions;
678
+ anchored;
679
+ ignoreCase;
680
+ /**
681
+ * The position each state was last reached at. One visit per state per
682
+ * position is exactly what turns the exponent into a product.
683
+ */
684
+ visited;
685
+ /** Each visit pushes at most two states, so this cannot overflow. */
686
+ stack;
687
+ live;
688
+ next;
689
+ constructor(code, anchored, ignoreCase) {
690
+ const size = code.length;
691
+ this.ops = new Uint8Array(size);
692
+ this.first = new Int32Array(size);
693
+ this.second = new Int32Array(size);
694
+ this.sets = new Array(size).fill(NOTHING);
695
+ this.assertions = new Array(size).fill('start');
696
+ this.anchored = anchored;
697
+ this.ignoreCase = ignoreCase;
698
+ this.visited = new Int32Array(size).fill(-1);
699
+ this.stack = new Int32Array(2 * size + 1);
700
+ this.live = new Int32Array(size);
701
+ this.next = new Int32Array(size);
702
+ code.forEach((instruction, pc) => {
703
+ switch (instruction.op) {
704
+ case 'char':
705
+ this.ops[pc] = OP_CHAR;
706
+ this.sets[pc] = instruction.set;
707
+ break;
708
+ case 'split':
709
+ this.ops[pc] = OP_SPLIT;
710
+ this.first[pc] = instruction.x;
711
+ this.second[pc] = instruction.y;
712
+ break;
713
+ case 'jump':
714
+ this.ops[pc] = OP_JUMP;
715
+ this.first[pc] = instruction.to;
716
+ break;
717
+ case 'assert':
718
+ this.ops[pc] = OP_ASSERT;
719
+ this.assertions[pc] = instruction.assertion;
720
+ break;
721
+ case 'match':
722
+ this.ops[pc] = OP_MATCH;
723
+ break;
724
+ }
725
+ });
726
+ }
727
+ test(subject) {
728
+ const length = subject.length;
729
+ this.visited.fill(-1);
730
+ let live = this.live;
731
+ let next = this.next;
732
+ let alive = 0;
733
+ for (let position = 0; position <= length; position += 1) {
734
+ if (position === 0 || !this.anchored) {
735
+ alive = this.seed(live, alive, 0, subject, position);
736
+ if (alive < 0)
737
+ return true;
738
+ }
739
+ else if (alive === 0) {
740
+ // An anchored pattern gets one chance, and every thread from it has died.
741
+ return false;
742
+ }
743
+ if (position === length)
744
+ return false;
745
+ const char = subject.charCodeAt(position);
746
+ let arriving = 0;
747
+ for (let i = 0; i < alive; i += 1) {
748
+ const pc = live[i];
749
+ const set = this.sets[pc];
750
+ if (!(this.ignoreCase ? accepts(set, char) : acceptsExactly(set, char)))
751
+ continue;
752
+ arriving = this.seed(next, arriving, pc + 1, subject, position + 1);
753
+ if (arriving < 0)
754
+ return true;
755
+ }
756
+ const spent = live;
757
+ live = next;
758
+ next = spent;
759
+ alive = arriving;
760
+ }
761
+ return false;
762
+ }
763
+ /**
764
+ * Follows every zero-width transition out of one state, appending the
765
+ * character states it reaches to `list`.
766
+ *
767
+ * Returns the new length of the list, or -1 the moment a thread reaches the
768
+ * end of the pattern: the only question is whether a match exists, so there
769
+ * is no leftmost-longest to settle and no captures to keep, and the first
770
+ * thread to arrive is the answer.
771
+ *
772
+ * A stray thread at state 0 changes no answer: an unanchored pattern seeds it
773
+ * at every position anyway, and an anchored one opens by asserting the start.
774
+ * That makes two mutants here equivalent, with no test to write - the loop
775
+ * running once past an empty stack, and a jump read as a split, both of which
776
+ * add exactly that thread.
777
+ */
778
+ seed(list, length, start, subject, position) {
779
+ const { ops, first, second, visited, stack } = this;
780
+ let count = length;
781
+ let top = 0;
782
+ stack[top++] = start;
783
+ while (top > 0) {
784
+ const pc = stack[--top];
785
+ if (visited[pc] === position)
786
+ continue;
787
+ visited[pc] = position;
788
+ switch (ops[pc]) {
789
+ case OP_MATCH:
790
+ return -1;
791
+ case OP_JUMP:
792
+ stack[top++] = first[pc];
793
+ break;
794
+ case OP_SPLIT:
795
+ stack[top++] = second[pc];
796
+ stack[top++] = first[pc];
797
+ break;
798
+ case OP_ASSERT:
799
+ if (holds(this.assertions[pc], subject, position))
800
+ stack[top++] = pc + 1;
801
+ break;
802
+ default:
803
+ list[count++] = pc;
804
+ }
805
+ }
806
+ return count;
807
+ }
808
+ }
809
+ function holds(assertion, subject, position) {
810
+ switch (assertion) {
811
+ case 'start':
812
+ return position === 0;
813
+ case 'end':
814
+ return position === subject.length;
815
+ default: {
816
+ const before = position > 0 && isWordChar(subject.charCodeAt(position - 1));
817
+ const after = position < subject.length && isWordChar(subject.charCodeAt(position));
818
+ return (before !== after) === (assertion === 'word-boundary');
819
+ }
820
+ }
821
+ }
822
+ //# sourceMappingURL=regex.js.map