@descent-vtt/spec-graph 0.4.0 → 0.5.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,708 @@
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 case-insensitive and unanchored because that is what `~=` has
39
+ * always been - `new RegExp(source, 'i')` and `.test()`. There are no capture
40
+ * groups at all: nothing asks where the match was, only whether there was one,
41
+ * which is why this is a plain NFA simulation rather than a Pike VM.
42
+ *
43
+ * See ADR-0017.
44
+ */
45
+ /* -------------------------------------------------------------------------- */
46
+ /* Errors */
47
+ /* -------------------------------------------------------------------------- */
48
+ /** A pattern that will not compile, and where in it the problem starts. */
49
+ export class PatternError extends Error {
50
+ /** Offset into the pattern source. */
51
+ offset;
52
+ constructor(message, offset) {
53
+ super(message);
54
+ this.name = 'PatternError';
55
+ this.offset = offset;
56
+ }
57
+ }
58
+ /**
59
+ * Ceiling on the compiled program.
60
+ *
61
+ * Counted repetition is compiled by copying - `(ab){20}` is twenty copies -
62
+ * which is part of what keeps the simulation flat, and also what makes
63
+ * `(a{99}){99}` into ten thousand states. The bound has to live somewhere, and
64
+ * a number checked while compiling is the only place it can be enforced without
65
+ * measuring anything at match time.
66
+ *
67
+ * Far above any real selector: the widest pattern in this repository's own
68
+ * tests compiles to seventeen states.
69
+ */
70
+ const MAX_PROGRAM = 4096;
71
+ /** `.` - anything but a line terminator, which is the default `RegExp` reading. */
72
+ const DOT = { negated: true, ranges: [[0x0a, 0x0a], [0x0d, 0x0d], [0x2028, 0x2029]] };
73
+ const DIGIT = [[0x30, 0x39]];
74
+ const WORD = [
75
+ [0x30, 0x39],
76
+ [0x41, 0x5a],
77
+ [0x5f, 0x5f],
78
+ [0x61, 0x7a],
79
+ ];
80
+ /** `\s`, taken from the specification rather than guessed at. */
81
+ const SPACE = [
82
+ [0x09, 0x0d],
83
+ [0x20, 0x20],
84
+ [0xa0, 0xa0],
85
+ [0x1680, 0x1680],
86
+ [0x2000, 0x200a],
87
+ [0x2028, 0x2029],
88
+ [0x202f, 0x202f],
89
+ [0x205f, 0x205f],
90
+ [0x3000, 0x3000],
91
+ [0xfeff, 0xfeff],
92
+ ];
93
+ /**
94
+ * Folds a code unit the way a case-insensitive `RegExp` does.
95
+ *
96
+ * This is `Canonicalize` from the language specification, non-Unicode mode:
97
+ * upper-case the character, keep it unchanged if that produced anything other
98
+ * than one code unit, and keep it unchanged if upper-casing dragged a
99
+ * non-ASCII character into ASCII. The last clause is the one nobody expects and
100
+ * the reason U+017F (long s) does not match `s`.
101
+ */
102
+ function canonical(code) {
103
+ const upper = String.fromCharCode(code).toUpperCase();
104
+ if (upper.length !== 1)
105
+ return code;
106
+ const folded = upper.charCodeAt(0);
107
+ if (code >= 128 && folded < 128)
108
+ return code;
109
+ return folded;
110
+ }
111
+ /**
112
+ * Whether a set accepts a code unit, case-insensitively.
113
+ *
114
+ * The specification canonicalises both sides: a character matches a set when
115
+ * *some* member of the set folds to the same thing it does. A single-character
116
+ * member can be asked that exactly. A range cannot without a fold table for
117
+ * every code unit, so a range is asked the question backwards - does the
118
+ * character, or either of its own case forms, fall inside it - which is exact
119
+ * for every character whose fold is its own case change, and therefore for
120
+ * every range anyone has written.
121
+ */
122
+ function accepts(set, code) {
123
+ const folded = canonical(code);
124
+ let hit = false;
125
+ for (const [low, high] of set.ranges) {
126
+ if (low === high ? low === code || canonical(low) === folded : inRange(low, high, code)) {
127
+ hit = true;
128
+ break;
129
+ }
130
+ }
131
+ return set.negated ? !hit : hit;
132
+ }
133
+ /**
134
+ * Whether some member of a range folds to the same thing a code unit does.
135
+ *
136
+ * Asked backwards, by trying the character's own case forms against the raw
137
+ * range - and then checking that the form found there really does fold to the
138
+ * same place, which is the part that is easy to leave out. U+017F upper-cases
139
+ * to `S`, so without that check it matches `[A-Z]`, and it must not: the fold
140
+ * keeps it outside ASCII, which is the same clause `isWordChar` gets wrong if
141
+ * nobody is watching. The differential corpus caught both.
142
+ */
143
+ function inRange(low, high, code) {
144
+ if (code >= low && code <= high)
145
+ return true;
146
+ const folded = canonical(code);
147
+ const char = String.fromCharCode(code);
148
+ for (const variant of [char.toLowerCase(), char.toUpperCase()]) {
149
+ if (variant.length !== 1)
150
+ continue;
151
+ const candidate = variant.charCodeAt(0);
152
+ if (candidate >= low && candidate <= high && canonical(candidate) === folded)
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ function within(ranges, code) {
158
+ for (const [low, high] of ranges) {
159
+ if (code >= low && code <= high)
160
+ return true;
161
+ }
162
+ return false;
163
+ }
164
+ /**
165
+ * Word characters for `\b`, which is exactly `\w` here.
166
+ *
167
+ * The specification does widen this set under case-insensitivity - to every
168
+ * character that folds into it, which is U+017F (long s) and U+212A (the
169
+ * Kelvin sign) - but only in Unicode mode, where `Canonicalize` has no clause
170
+ * keeping a non-ASCII character out of ASCII. This is the non-Unicode dialect,
171
+ * so both stay outside and the table for `\w` answers for `\b` as well.
172
+ *
173
+ * Written down because the first version of this function widened the set, and
174
+ * the differential test against `RegExp` is what said otherwise. Reading the
175
+ * clause was not enough; running it was. See ADR-0017.
176
+ */
177
+ function isWordChar(code) {
178
+ return within(WORD, code);
179
+ }
180
+ /* -------------------------------------------------------------------------- */
181
+ /* Parsing */
182
+ /* -------------------------------------------------------------------------- */
183
+ class PatternParser {
184
+ source;
185
+ position = 0;
186
+ constructor(source) {
187
+ this.source = source;
188
+ }
189
+ parse() {
190
+ const node = this.parseAlternation();
191
+ // Every character is an atom except the two that close something, and
192
+ // `parseSequence` stops on both, so anything left over is an unopened `)`.
193
+ if (this.position < this.source.length)
194
+ throw new PatternError('unmatched ")"', this.position);
195
+ return node;
196
+ }
197
+ parseAlternation() {
198
+ const options = [this.parseSequence()];
199
+ while (this.peek() === '|') {
200
+ this.position += 1;
201
+ options.push(this.parseSequence());
202
+ }
203
+ return options.length === 1 ? options[0] : { kind: 'alternate', options };
204
+ }
205
+ parseSequence() {
206
+ const parts = [];
207
+ for (;;) {
208
+ const char = this.peek();
209
+ if (char === undefined || char === '|' || char === ')')
210
+ break;
211
+ parts.push(this.parseQuantified());
212
+ }
213
+ if (parts.length === 0)
214
+ return { kind: 'empty' };
215
+ return parts.length === 1 ? parts[0] : { kind: 'concat', parts };
216
+ }
217
+ parseQuantified() {
218
+ const start = this.position;
219
+ // A group makes its contents quantifiable: `RegExp` rejects `\b*` and
220
+ // accepts `(\b)*`, which is the same nothing said twice, and this parser
221
+ // treats a group as transparent - so whether one was written is the only
222
+ // thing left that distinguishes the two.
223
+ const grouped = this.peek() === '(';
224
+ const atom = this.parseAtom();
225
+ const bounds = this.parseQuantifier();
226
+ if (bounds === null)
227
+ return atom;
228
+ if (atom.kind === 'assert' && !grouped) {
229
+ throw new PatternError('nothing to repeat: a quantifier cannot follow an anchor', start);
230
+ }
231
+ return { kind: 'repeat', node: atom, min: bounds.min, max: bounds.max };
232
+ }
233
+ /** `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`, and a trailing `?` that is ignored. */
234
+ parseQuantifier() {
235
+ const char = this.peek();
236
+ let bounds = null;
237
+ if (char === '*')
238
+ bounds = { min: 0, max: Infinity };
239
+ else if (char === '+')
240
+ bounds = { min: 1, max: Infinity };
241
+ else if (char === '?')
242
+ bounds = { min: 0, max: 1 };
243
+ if (bounds !== null)
244
+ this.position += 1;
245
+ else if (char === '{')
246
+ bounds = this.parseBraces();
247
+ if (bounds === null)
248
+ return null;
249
+ // Laziness decides *which* match is found, and nothing here asks: the
250
+ // question is whether one exists, and a lazy quantifier accepts the same
251
+ // language as a greedy one.
252
+ if (this.peek() === '?')
253
+ this.position += 1;
254
+ return bounds;
255
+ }
256
+ /**
257
+ * Reads `{n,m}`, or nothing.
258
+ *
259
+ * A `{` that does not open a counted repetition is a literal brace, which is
260
+ * what `RegExp` does with `a{` and what any pattern written against a
261
+ * document full of `{0}` placeholders depends on.
262
+ */
263
+ parseBraces() {
264
+ const match = /^\{(\d+)(?:(,)(\d*))?\}/.exec(this.source.slice(this.position));
265
+ if (!match)
266
+ return null;
267
+ const min = Number(match[1]);
268
+ const max = match[2] === undefined ? min : match[3] === '' ? Infinity : Number(match[3]);
269
+ if (min > max)
270
+ throw new PatternError('numbers out of order in a counted repetition', this.position);
271
+ this.position += match[0].length;
272
+ return { min, max };
273
+ }
274
+ parseAtom() {
275
+ const start = this.position;
276
+ const char = this.peek();
277
+ switch (char) {
278
+ case undefined:
279
+ throw new PatternError('pattern ends where an expression was expected', start);
280
+ case '(':
281
+ return this.parseGroup();
282
+ case '[':
283
+ return { kind: 'char', set: this.parseClass() };
284
+ case '.':
285
+ this.position += 1;
286
+ return { kind: 'char', set: DOT };
287
+ case '^':
288
+ this.position += 1;
289
+ return { kind: 'assert', assertion: 'start' };
290
+ case '$':
291
+ this.position += 1;
292
+ return { kind: 'assert', assertion: 'end' };
293
+ case '*':
294
+ case '+':
295
+ throw new PatternError(`nothing to repeat before "${char}"`, start);
296
+ case '\\':
297
+ return this.parseEscape();
298
+ default:
299
+ this.position += 1;
300
+ return { kind: 'char', set: literal(char.charCodeAt(0)) };
301
+ }
302
+ }
303
+ /**
304
+ * A group, which is never a capture.
305
+ *
306
+ * `(a)` and `(?:a)` compile to the same automaton because nothing downstream
307
+ * can ask what `(a)` caught. A named group is accepted and its name dropped,
308
+ * so a pattern copied out of somewhere else keeps working.
309
+ */
310
+ parseGroup() {
311
+ const start = this.position;
312
+ this.position += 1;
313
+ if (this.source.startsWith('?:', this.position)) {
314
+ this.position += 2;
315
+ }
316
+ else if (this.peek() === '?') {
317
+ const named = /^\?<([A-Za-z_$][\w$]*)>/.exec(this.source.slice(this.position));
318
+ if (named) {
319
+ this.position += named[0].length;
320
+ }
321
+ else if (/^\?(?:=|!|<=|<!)/.test(this.source.slice(this.position))) {
322
+ 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);
323
+ }
324
+ else {
325
+ throw new PatternError('unsupported group: expected (?:...) or (?<name>...)', start);
326
+ }
327
+ }
328
+ const inner = this.parseAlternation();
329
+ if (this.peek() !== ')')
330
+ throw new PatternError('unmatched "("', start);
331
+ this.position += 1;
332
+ return inner;
333
+ }
334
+ /**
335
+ * Reads a `[...]` class.
336
+ *
337
+ * `[]` matches nothing and `[^]` matches anything, which reads like a mistake
338
+ * and is what every JavaScript engine does: the class ends at the first `]`,
339
+ * even when that `]` is the first character inside it.
340
+ */
341
+ parseClass() {
342
+ const start = this.position;
343
+ this.position += 1;
344
+ const negated = this.peek() === '^';
345
+ if (negated)
346
+ this.position += 1;
347
+ const ranges = [];
348
+ while (this.position < this.source.length && this.peek() !== ']') {
349
+ const low = this.parseClassAtom();
350
+ // A `-` between two single characters is a range. Anywhere else - at
351
+ // either end of the class, or beside a `\d`-style set, which has no
352
+ // endpoint - it is a literal hyphen, exactly as Annex B has it.
353
+ if (this.peek() !== '-' || this.source[this.position + 1] === ']' || this.position + 1 >= this.source.length) {
354
+ push(ranges, low);
355
+ continue;
356
+ }
357
+ const dash = this.position;
358
+ this.position += 1;
359
+ const high = this.parseClassAtom();
360
+ if (typeof low === 'number' && typeof high === 'number') {
361
+ if (low > high)
362
+ throw new PatternError('characters out of order in a character class', dash);
363
+ ranges.push([low, high]);
364
+ continue;
365
+ }
366
+ push(ranges, low);
367
+ ranges.push([0x2d, 0x2d]);
368
+ push(ranges, high);
369
+ }
370
+ if (this.peek() !== ']')
371
+ throw new PatternError('unterminated character class', start);
372
+ this.position += 1;
373
+ return { negated, ranges };
374
+ }
375
+ /** One member of a class: a code unit, or a whole set for `\d` and its kind. */
376
+ parseClassAtom() {
377
+ if (this.peek() !== '\\') {
378
+ const code = this.peek().charCodeAt(0);
379
+ this.position += 1;
380
+ return code;
381
+ }
382
+ // `\b` is a backspace inside a class and a word boundary outside one, which
383
+ // is the only place the two readings of an escape diverge.
384
+ if (this.source[this.position + 1] === 'b') {
385
+ this.position += 2;
386
+ return 0x08;
387
+ }
388
+ if (this.source[this.position + 1] === 'B') {
389
+ throw new PatternError('"\\B" is an assertion, not a character, so it cannot be in a class', this.position);
390
+ }
391
+ const set = this.parseCharacterEscape();
392
+ if (!set.negated && set.ranges.length === 1) {
393
+ const [low, high] = set.ranges[0];
394
+ if (low === high)
395
+ return low;
396
+ }
397
+ return set.negated ? invert(set.ranges) : set.ranges;
398
+ }
399
+ /** An escape outside a class, where `\b` and `\B` are assertions. */
400
+ parseEscape() {
401
+ const next = this.source[this.position + 1];
402
+ if (next === 'b' || next === 'B') {
403
+ this.position += 2;
404
+ return { kind: 'assert', assertion: next === 'b' ? 'word-boundary' : 'not-word-boundary' };
405
+ }
406
+ return { kind: 'char', set: this.parseCharacterEscape() };
407
+ }
408
+ /** Every escape that stands for a character or a set of them. */
409
+ parseCharacterEscape() {
410
+ const start = this.position;
411
+ this.position += 1;
412
+ const char = this.peek();
413
+ if (char === undefined)
414
+ throw new PatternError('pattern ends with a backslash', start);
415
+ this.position += 1;
416
+ switch (char) {
417
+ case 'd':
418
+ return { negated: false, ranges: DIGIT };
419
+ case 'D':
420
+ return { negated: true, ranges: DIGIT };
421
+ case 'w':
422
+ return { negated: false, ranges: WORD };
423
+ case 'W':
424
+ return { negated: true, ranges: WORD };
425
+ case 's':
426
+ return { negated: false, ranges: SPACE };
427
+ case 'S':
428
+ return { negated: true, ranges: SPACE };
429
+ case 'n':
430
+ return literal(0x0a);
431
+ case 'r':
432
+ return literal(0x0d);
433
+ case 't':
434
+ return literal(0x09);
435
+ case 'f':
436
+ return literal(0x0c);
437
+ case 'v':
438
+ return literal(0x0b);
439
+ case '0':
440
+ if (/^\d/.test(this.source.slice(this.position))) {
441
+ throw new PatternError('an octal escape is not supported - write \\xHH or \\uHHHH', start);
442
+ }
443
+ return literal(0);
444
+ case 'x':
445
+ return literal(this.readHex(2, start));
446
+ case 'u':
447
+ return literal(this.readHex(4, start));
448
+ case 'c': {
449
+ const letter = this.peek();
450
+ if (letter === undefined || !/[A-Za-z]/.test(letter)) {
451
+ throw new PatternError('expected a letter after "\\c"', start);
452
+ }
453
+ this.position += 1;
454
+ return literal(letter.charCodeAt(0) % 32);
455
+ }
456
+ case 'k':
457
+ throw new PatternError('a named backreference is not supported: a backreference is not a regular expression, and this is an automaton', start);
458
+ case 'p':
459
+ case 'P':
460
+ throw new PatternError(`"\\${char}" needs a table of Unicode properties, which this matcher does not carry - write the characters out as a [class]`, start);
461
+ default:
462
+ if (/[1-9]/.test(char)) {
463
+ throw new PatternError('a backreference is not supported: it is not a regular expression, and this is an automaton', start);
464
+ }
465
+ if (/[A-Za-z]/.test(char)) {
466
+ // `RegExp` reads an unknown letter escape as the letter itself, so
467
+ // `\A` quietly matches a capital A and `\z` a lower-case z. Saying so
468
+ // is worth more than agreeing with a rule nobody wanted.
469
+ throw new PatternError(`unknown escape "\\${char}"`, start);
470
+ }
471
+ return literal(char.charCodeAt(0));
472
+ }
473
+ }
474
+ readHex(digits, start) {
475
+ const raw = this.source.slice(this.position, this.position + digits);
476
+ if (raw.length !== digits || !/^[0-9a-fA-F]+$/.test(raw)) {
477
+ throw new PatternError(`expected ${digits} hex digits after "\\${this.source[start + 1]}"`, start);
478
+ }
479
+ this.position += digits;
480
+ return Number.parseInt(raw, 16);
481
+ }
482
+ peek() {
483
+ return this.source[this.position];
484
+ }
485
+ }
486
+ function literal(code) {
487
+ return { negated: false, ranges: [[code, code]] };
488
+ }
489
+ function push(ranges, atom) {
490
+ if (typeof atom === 'number')
491
+ ranges.push([atom, atom]);
492
+ else
493
+ ranges.push(...atom);
494
+ }
495
+ /**
496
+ * Complement of a range list, so `[\D]` can sit inside a class.
497
+ *
498
+ * A negated escape is a set with no endpoints, and a class is a union of
499
+ * ranges, so the only way to put one inside the other is to spell out what it
500
+ * leaves.
501
+ */
502
+ function invert(ranges) {
503
+ const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
504
+ const out = [];
505
+ let cursor = 0;
506
+ for (const [low, high] of sorted) {
507
+ if (low > cursor)
508
+ out.push([cursor, low - 1]);
509
+ cursor = Math.max(cursor, high + 1);
510
+ }
511
+ if (cursor <= 0xffff)
512
+ out.push([cursor, 0xffff]);
513
+ return out;
514
+ }
515
+ class Compiler {
516
+ code = [];
517
+ add(instruction) {
518
+ if (this.code.length >= MAX_PROGRAM) {
519
+ throw new PatternError(`pattern is too large: counted repetition is compiled by copying, and this needs more than ${MAX_PROGRAM} states`, 0);
520
+ }
521
+ this.code.push(instruction);
522
+ return instruction;
523
+ }
524
+ get here() {
525
+ return this.code.length;
526
+ }
527
+ emitMatch() {
528
+ this.add({ op: 'match' });
529
+ }
530
+ compile(node) {
531
+ switch (node.kind) {
532
+ case 'empty':
533
+ return;
534
+ case 'char':
535
+ this.add({ op: 'char', set: node.set });
536
+ return;
537
+ case 'assert':
538
+ this.add({ op: 'assert', assertion: node.assertion });
539
+ return;
540
+ case 'concat':
541
+ for (const part of node.parts)
542
+ this.compile(part);
543
+ return;
544
+ case 'alternate': {
545
+ // One split per option but the last. Each split falls through to its
546
+ // own branch and points its second arm at the next split; each branch
547
+ // ends in a jump past all of them.
548
+ const ends = [];
549
+ for (let i = 0; i < node.options.length - 1; i += 1) {
550
+ const split = this.add({ op: 'split', x: 0, y: 0 });
551
+ split.x = this.here;
552
+ this.compile(node.options[i]);
553
+ ends.push(this.add({ op: 'jump', to: 0 }));
554
+ split.y = this.here;
555
+ }
556
+ this.compile(node.options[node.options.length - 1]);
557
+ for (const end of ends)
558
+ end.to = this.here;
559
+ return;
560
+ }
561
+ case 'repeat':
562
+ this.repeat(node.node, node.min, node.max);
563
+ return;
564
+ }
565
+ }
566
+ repeat(node, min, max) {
567
+ for (let i = 0; i < min; i += 1)
568
+ this.compile(node);
569
+ if (max === Infinity) {
570
+ const split = this.add({ op: 'split', x: 0, y: 0 });
571
+ const body = this.here;
572
+ split.x = body;
573
+ this.compile(node);
574
+ this.add({ op: 'jump', to: body - 1 });
575
+ split.y = this.here;
576
+ return;
577
+ }
578
+ // `{2,5}` is two copies and then three optional ones, each able to leave
579
+ // for the far end rather than for the next copy.
580
+ const exits = [];
581
+ for (let i = min; i < max; i += 1) {
582
+ const split = this.add({ op: 'split', x: 0, y: 0 });
583
+ split.x = this.here;
584
+ exits.push(split);
585
+ this.compile(node);
586
+ }
587
+ for (const split of exits)
588
+ split.y = this.here;
589
+ }
590
+ }
591
+ /**
592
+ * Compiles a pattern into a matcher.
593
+ *
594
+ * Throws {@link PatternError} on anything it cannot run in linear time, which
595
+ * is the point: the alternative to a message here is a build that stops.
596
+ */
597
+ export function compilePattern(source) {
598
+ const tree = new PatternParser(source).parse();
599
+ const compiler = new Compiler();
600
+ compiler.compile(tree);
601
+ compiler.emitMatch();
602
+ const code = compiler.code;
603
+ // A pattern that begins by asserting the start of the subject cannot match
604
+ // anywhere else, so the simulation stops seeding new threads at every
605
+ // position. Nothing depends on this being noticed - it is the difference
606
+ // between one pass and two over a long body.
607
+ const anchored = startsAnchored(tree);
608
+ return {
609
+ size: code.length,
610
+ test: (subject) => run(code, subject, anchored),
611
+ };
612
+ }
613
+ /** True when every path through the pattern opens by asserting the start. */
614
+ function startsAnchored(node) {
615
+ switch (node.kind) {
616
+ case 'assert':
617
+ return node.assertion === 'start';
618
+ case 'concat': {
619
+ const first = node.parts[0];
620
+ return first !== undefined && startsAnchored(first);
621
+ }
622
+ case 'alternate':
623
+ return node.options.every((option) => startsAnchored(option));
624
+ case 'repeat':
625
+ return node.min > 0 && startsAnchored(node.node);
626
+ default:
627
+ return false;
628
+ }
629
+ }
630
+ function run(code, subject, anchored) {
631
+ const length = subject.length;
632
+ // The position each state was last reached at. One visit per state per
633
+ // position is exactly what turns the exponent into a product.
634
+ const visited = new Int32Array(code.length).fill(-1);
635
+ let live = [];
636
+ for (let position = 0; position <= length; position += 1) {
637
+ if (position === 0 || !anchored) {
638
+ if (seedFrom(code, visited, live, 0, subject, position))
639
+ return true;
640
+ }
641
+ else if (live.length === 0) {
642
+ // An anchored pattern gets one chance, and every thread from it has died.
643
+ return false;
644
+ }
645
+ if (position === length)
646
+ return false;
647
+ const char = subject.charCodeAt(position);
648
+ const next = [];
649
+ for (const pc of live) {
650
+ const instruction = code[pc];
651
+ if (instruction.op !== 'char' || !accepts(instruction.set, char))
652
+ continue;
653
+ if (seedFrom(code, visited, next, pc + 1, subject, position + 1))
654
+ return true;
655
+ }
656
+ live = next;
657
+ }
658
+ return false;
659
+ }
660
+ /**
661
+ * Follows every zero-width transition out of one state.
662
+ *
663
+ * Returns true the moment a thread reaches the end of the pattern: the only
664
+ * question is whether a match exists, so there is no leftmost-longest to settle
665
+ * and no captures to keep, and the first thread to arrive is the answer.
666
+ */
667
+ function seedFrom(code, visited, list, start, subject, position) {
668
+ const stack = [start];
669
+ while (stack.length > 0) {
670
+ const pc = stack.pop();
671
+ if (visited[pc] === position)
672
+ continue;
673
+ visited[pc] = position;
674
+ const instruction = code[pc];
675
+ switch (instruction.op) {
676
+ case 'match':
677
+ return true;
678
+ case 'jump':
679
+ stack.push(instruction.to);
680
+ break;
681
+ case 'split':
682
+ stack.push(instruction.y, instruction.x);
683
+ break;
684
+ case 'assert':
685
+ if (holds(instruction.assertion, subject, position))
686
+ stack.push(pc + 1);
687
+ break;
688
+ case 'char':
689
+ list.push(pc);
690
+ break;
691
+ }
692
+ }
693
+ return false;
694
+ }
695
+ function holds(assertion, subject, position) {
696
+ switch (assertion) {
697
+ case 'start':
698
+ return position === 0;
699
+ case 'end':
700
+ return position === subject.length;
701
+ default: {
702
+ const before = position > 0 && isWordChar(subject.charCodeAt(position - 1));
703
+ const after = position < subject.length && isWordChar(subject.charCodeAt(position));
704
+ return (before !== after) === (assertion === 'word-boundary');
705
+ }
706
+ }
707
+ }
708
+ //# sourceMappingURL=regex.js.map