grammatik 0.1.0 → 0.1.2

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/esm/index.js CHANGED
@@ -315,7 +315,338 @@ function firstCharSet(source, ignoreCase) {
315
315
  return found;
316
316
  }
317
317
 
318
- // src/lexer.js
318
+ // src/scan.js
319
+ var DEFER = -2;
320
+ var CLASS = 0;
321
+ var SEQ2 = 1;
322
+ var KEYWORD = 2;
323
+ var LINE = 3;
324
+ var QUOTED = 4;
325
+ var NUMBER = 5;
326
+ var STRING = 6;
327
+ var SCANNER_KINDS = Object.freeze({ CLASS, SEQ2, KEYWORD, LINE, QUOTED, NUMBER, STRING });
328
+ var NONE = new Uint8Array(128);
329
+ function make(kind, fields) {
330
+ return {
331
+ kind,
332
+ wide: false,
333
+ first: NONE,
334
+ rest: NONE,
335
+ second: NONE,
336
+ look: NONE,
337
+ lower: [],
338
+ upper: [],
339
+ prefix: "",
340
+ quote: -1,
341
+ sign: false,
342
+ jsonInt: false,
343
+ frac: false,
344
+ exp: false,
345
+ ctrl: false,
346
+ uni: false,
347
+ escAny: false,
348
+ esc: NONE,
349
+ ...fields
350
+ };
351
+ }
352
+ var SHORTHAND = {
353
+ d: (t) => {
354
+ for (let k = 48; k < 58; k++) t[k] = 1;
355
+ },
356
+ w: (t) => {
357
+ for (let k = 0; k < 128; k++) if (/\w/.test(String.fromCharCode(k))) t[k] = 1;
358
+ },
359
+ s: (t) => {
360
+ for (const k of [9, 10, 11, 12, 13, 32]) t[k] = 1;
361
+ }
362
+ };
363
+ var SIMPLE_ESCAPES2 = { t: " ", r: "\r", n: "\n", f: "\f", v: "\v" };
364
+ var ALNUM = /[a-zA-Z0-9]/;
365
+ function parseClass(body) {
366
+ if (body.length === 0 || body[0] === "^") return null;
367
+ const table = new Uint8Array(128);
368
+ for (let i = 0; i < body.length; i++) {
369
+ let c = body[i];
370
+ if (c === "\\") {
371
+ const n = body[++i];
372
+ if (n === "d" || n === "w") {
373
+ SHORTHAND[n](table);
374
+ continue;
375
+ }
376
+ if (n === void 0) return null;
377
+ if (ALNUM.test(n) && !(n in SIMPLE_ESCAPES2)) return null;
378
+ c = SIMPLE_ESCAPES2[n] ?? n;
379
+ }
380
+ if (body[i + 1] === "-" && i + 2 < body.length) {
381
+ let hi = body[i + 2];
382
+ let advance = 2;
383
+ if (hi === "\\") {
384
+ hi = body[i + 3];
385
+ advance = 3;
386
+ if (hi === void 0 || ALNUM.test(hi)) return null;
387
+ }
388
+ const from = c.charCodeAt(0);
389
+ const to = hi.charCodeAt(0);
390
+ if (from > to || to >= 128) return null;
391
+ for (let k = from; k <= to; k++) table[k] = 1;
392
+ i += advance;
393
+ continue;
394
+ }
395
+ const code = c.charCodeAt(0);
396
+ if (code >= 128) return null;
397
+ table[code] = 1;
398
+ }
399
+ return table;
400
+ }
401
+ var BODY = String.raw`((?:[^\]\\]|\\.)*)`;
402
+ var RE_CLASS_RUN = new RegExp(String.raw`^\[${BODY}\]([+*])$`);
403
+ var RE_CLASS_PAIR = new RegExp(String.raw`^\[${BODY}\]\[${BODY}\]\*$`);
404
+ var RE_CLASS_SEQ = new RegExp(String.raw`^\[${BODY}\]\+\[${BODY}\]\+$`);
405
+ var RE_SHORTHAND = /^\\([sdw])[+*]$/;
406
+ var RE_KEYWORD = new RegExp(String.raw`^([A-Za-z0-9_]+)\(\?!\[${BODY}\]\)$`);
407
+ var RE_LINE = new RegExp(String.raw`^([^\\\[\]()*+?.|^$]+)\[\^${BODY}\]\*$`);
408
+ var RE_QUOTED = /^(['"])\(\?:\1\1\|\[\^\1\]\)\*\1$/;
409
+ var RE_NUMBER = /^(-\?)?(?:(\(\?:0\|\[1-9\]\\d\*\))|\\d\+)(\(\?:\\\.\\d\+\)\?)?(\(\?:\[eE\]\[\+-\]\?\\d\+\)\?)?$/;
410
+ var RE_STRING_ESC = new RegExp(String.raw`^(["'])\(\?:\[\^\1\\\\(\\u0000-\\u001f|\\x00-\\x1f)?\]\|\\\\\(\?:\[${BODY}\](\|u\[0-9a-fA-F\]\{4\})?\)\)\*\1$`);
411
+ var RE_STRING_ANY = new RegExp(String.raw`^(["'])\(\?:\[\^\1\\\\\]\|\\\\\.\)\*\1$`);
412
+ function buildScanner(source, ignoreCase) {
413
+ let m = RE_KEYWORD.exec(source);
414
+ if (m) {
415
+ const look = parseClass(m[2]);
416
+ if (look === null) return null;
417
+ const word = m[1];
418
+ const lower = [];
419
+ const upper = [];
420
+ for (const ch of word) {
421
+ lower.push((ignoreCase ? ch.toLowerCase() : ch).charCodeAt(0));
422
+ upper.push((ignoreCase ? ch.toUpperCase() : ch).charCodeAt(0));
423
+ }
424
+ return make(KEYWORD, { look, lower, upper });
425
+ }
426
+ if (ignoreCase) return null;
427
+ m = RE_STRING_ESC.exec(source);
428
+ if (m) {
429
+ const esc = parseClass(m[3]);
430
+ if (esc === null) return null;
431
+ return make(STRING, { quote: m[1].charCodeAt(0), ctrl: m[2] !== void 0, uni: m[4] !== void 0, esc });
432
+ }
433
+ m = RE_STRING_ANY.exec(source);
434
+ if (m) return make(STRING, { quote: m[1].charCodeAt(0), escAny: true });
435
+ m = RE_NUMBER.exec(source.replace(/\[0-9\]/g, "\\d"));
436
+ if (m && (m[1] || m[2] || m[3] || m[4])) {
437
+ return make(NUMBER, { sign: m[1] !== void 0, jsonInt: m[2] !== void 0, frac: m[3] !== void 0, exp: m[4] !== void 0 });
438
+ }
439
+ m = RE_CLASS_RUN.exec(source);
440
+ if (m) {
441
+ const table = parseClass(m[1]);
442
+ return table && make(CLASS, { first: table, rest: table });
443
+ }
444
+ m = RE_CLASS_PAIR.exec(source);
445
+ if (m) {
446
+ const first = parseClass(m[1]);
447
+ const rest = parseClass(m[2]);
448
+ return first && rest && make(CLASS, { first, rest });
449
+ }
450
+ m = RE_CLASS_SEQ.exec(source);
451
+ if (m) {
452
+ const first = parseClass(m[1]);
453
+ const second = parseClass(m[2]);
454
+ if (first && second && first.every((v, i) => !(v && second[i]))) {
455
+ return make(SEQ2, { first, rest: first, second });
456
+ }
457
+ return null;
458
+ }
459
+ m = RE_SHORTHAND.exec(source);
460
+ if (m) {
461
+ const table = new Uint8Array(128);
462
+ SHORTHAND[m[1]](table);
463
+ return make(CLASS, { first: table, rest: table, wide: m[1] === "s" });
464
+ }
465
+ m = RE_QUOTED.exec(source);
466
+ if (m) return make(QUOTED, { quote: m[1].charCodeAt(0) });
467
+ m = RE_LINE.exec(source);
468
+ if (m) {
469
+ const stop = parseClass(m[2]);
470
+ return stop && make(LINE, { prefix: m[1], look: stop });
471
+ }
472
+ return null;
473
+ }
474
+ var skipDigits = (text, i, length) => {
475
+ while (i < length) {
476
+ const c = text.charCodeAt(i);
477
+ if (c < 48 || c > 57) break;
478
+ i++;
479
+ }
480
+ return i;
481
+ };
482
+ var isHex = (c) => c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
483
+ function scanEnd(sc, text, offset, length) {
484
+ switch (sc.kind) {
485
+ case CLASS: {
486
+ const code = text.charCodeAt(offset);
487
+ if (code >= 128) return sc.wide ? DEFER : -1;
488
+ if (sc.first[code] !== 1) return -1;
489
+ const rest = sc.rest;
490
+ let j = offset + 1;
491
+ while (j < length) {
492
+ const c = text.charCodeAt(j);
493
+ if (c >= 128) return sc.wide ? DEFER : j;
494
+ if (rest[c] !== 1) break;
495
+ j++;
496
+ }
497
+ return j;
498
+ }
499
+ case SEQ2: {
500
+ const c0 = text.charCodeAt(offset);
501
+ if (c0 >= 128 || sc.first[c0] !== 1) return -1;
502
+ const { first, second } = sc;
503
+ let j = offset + 1;
504
+ while (j < length) {
505
+ const c = text.charCodeAt(j);
506
+ if (c < 128 && first[c] === 1) j++;
507
+ else break;
508
+ }
509
+ const mid = j;
510
+ while (j < length) {
511
+ const c = text.charCodeAt(j);
512
+ if (c < 128 && second[c] === 1) j++;
513
+ else break;
514
+ }
515
+ return j > mid ? j : -1;
516
+ }
517
+ case KEYWORD: {
518
+ const { lower, upper } = sc;
519
+ const n = lower.length;
520
+ if (offset + n > length) return -1;
521
+ for (let i = 0; i < n; i++) {
522
+ const c = text.charCodeAt(offset + i);
523
+ if (c !== lower[i] && c !== upper[i]) return -1;
524
+ }
525
+ const end = offset + n;
526
+ if (end < length) {
527
+ const c = text.charCodeAt(end);
528
+ if (c < 128 && sc.look[c] === 1) return -1;
529
+ }
530
+ return end;
531
+ }
532
+ case LINE: {
533
+ if (!text.startsWith(sc.prefix, offset)) return -1;
534
+ let j = offset + sc.prefix.length;
535
+ const stop = sc.look;
536
+ while (j < length) {
537
+ const c = text.charCodeAt(j);
538
+ if (c < 128 && stop[c] === 1) break;
539
+ j++;
540
+ }
541
+ return j;
542
+ }
543
+ case NUMBER: {
544
+ let j = offset;
545
+ if (sc.sign && text.charCodeAt(j) === 45) j++;
546
+ const c = text.charCodeAt(j);
547
+ if (sc.jsonInt) {
548
+ if (c === 48) j++;
549
+ else if (c >= 49 && c <= 57) j = skipDigits(text, j + 1, length);
550
+ else return -1;
551
+ } else {
552
+ if (!(c >= 48 && c <= 57)) return -1;
553
+ j = skipDigits(text, j + 1, length);
554
+ }
555
+ if (sc.frac && text.charCodeAt(j) === 46) {
556
+ const k = skipDigits(text, j + 1, length);
557
+ if (k > j + 1) j = k;
558
+ }
559
+ if (sc.exp) {
560
+ const e = text.charCodeAt(j);
561
+ if (e === 101 || e === 69) {
562
+ let k = j + 1;
563
+ const s = text.charCodeAt(k);
564
+ if (s === 43 || s === 45) k++;
565
+ const end = skipDigits(text, k, length);
566
+ if (end > k) j = end;
567
+ }
568
+ }
569
+ return j;
570
+ }
571
+ case STRING: {
572
+ const q2 = sc.quote;
573
+ if (text.charCodeAt(offset) !== q2) return -1;
574
+ let j = offset + 1;
575
+ for (; ; ) {
576
+ if (j >= length) return -1;
577
+ const c = text.charCodeAt(j);
578
+ if (c === q2) return j + 1;
579
+ if (c === 92) {
580
+ const d = text.charCodeAt(j + 1);
581
+ if (sc.escAny) {
582
+ if (!(d >= 0) || d === 10 || d === 13 || d === 8232 || d === 8233) return -1;
583
+ j += 2;
584
+ } else if (d < 128 && sc.esc[d] === 1) {
585
+ j += 2;
586
+ } else if (sc.uni && d === 117 && isHex(text.charCodeAt(j + 2)) && isHex(text.charCodeAt(j + 3)) && isHex(text.charCodeAt(j + 4)) && isHex(text.charCodeAt(j + 5))) {
587
+ j += 6;
588
+ } else {
589
+ return -1;
590
+ }
591
+ continue;
592
+ }
593
+ if (sc.ctrl && c < 32) return -1;
594
+ j++;
595
+ }
596
+ }
597
+ default: {
598
+ const q2 = sc.quote;
599
+ if (text.charCodeAt(offset) !== q2) return -1;
600
+ const quote2 = String.fromCharCode(q2);
601
+ let fallback = -1;
602
+ let j = offset + 1;
603
+ for (; ; ) {
604
+ j = text.indexOf(quote2, j);
605
+ if (j === -1) return fallback;
606
+ if (text.charCodeAt(j + 1) !== q2) return j + 1;
607
+ fallback = j + 1;
608
+ j += 2;
609
+ }
610
+ }
611
+ }
612
+ }
613
+
614
+ // src/state.js
615
+ var FAIL = /* @__PURE__ */ Symbol("grammatik.fail");
616
+ var memoTables = (slots, length) => {
617
+ if (!slots) return null;
618
+ const tables = new Array(slots);
619
+ for (let i = 0; i < slots; i++) tables[i] = { ends: new Int32Array(length + 1), vals: new Array(length + 1) };
620
+ return tables;
621
+ };
622
+ var createState = (tokens, grammar, options = {}) => ({
623
+ tokens,
624
+ idx: 0,
625
+ maxIdx: 0,
626
+ expected: [],
627
+ grammar,
628
+ memo: options.memo ? /* @__PURE__ */ new Map() : null,
629
+ rm: memoTables(options.memoSlots, tokens.length),
630
+ // Entering a rule is one array store rather than an object allocation, and
631
+ // the stack is snapshotted only on a furthest failure — by the time an error
632
+ // is built it would otherwise have unwound to just the start rule.
633
+ ruleNames: [],
634
+ ruleDepth: 0,
635
+ failureStack: null,
636
+ track: options.track === true,
637
+ steps: 0,
638
+ maxSteps: options.maxSteps || 0
639
+ });
640
+ function expect(state, expectation) {
641
+ if (state.idx > state.maxIdx) {
642
+ state.maxIdx = state.idx;
643
+ if (!state.track) return;
644
+ state.expected = [];
645
+ state.failureStack = state.ruleNames.slice(0, state.ruleDepth);
646
+ }
647
+ if (!state.track || state.idx !== state.maxIdx) return;
648
+ if (!state.expected.includes(expectation)) state.expected.push(expectation);
649
+ }
319
650
  var LexError = class extends Error {
320
651
  /**
321
652
  * @param {string} message
@@ -329,6 +660,33 @@ var LexError = class extends Error {
329
660
  this.col = position.col;
330
661
  }
331
662
  };
663
+ var ParseError = class extends Error {
664
+ /**
665
+ * @param {string} message
666
+ * @param {{ token: import('./lexer.js').Token | null, expected: string[], ruleStack: string[] }} details
667
+ */
668
+ constructor(message, details) {
669
+ super(message);
670
+ this.name = "ParseError";
671
+ this.token = details.token;
672
+ this.expected = details.expected;
673
+ this.ruleStack = details.ruleStack;
674
+ }
675
+ };
676
+ var describeExpected = (expected) => expected.length === 0 ? "nothing" : expected.length === 1 ? expected[0] : `one of [${expected.join(", ")}]`;
677
+ function buildParseError(state) {
678
+ const token = state.tokens[state.maxIdx] || null;
679
+ const expected = [...state.expected].sort();
680
+ const ruleStack = state.failureStack || state.ruleNames.slice(0, state.ruleDepth);
681
+ const at = (t) => state.positions === "offset" ? `offset ${t.start}` : `line ${t.line} column ${t.col}`;
682
+ const found = token ? `--> ${token.image} <-- (${token.type}) at ${at(token)}` : "end of input";
683
+ return new ParseError(
684
+ `Expecting ${describeExpected(expected)} but found ${found}`,
685
+ { token, expected, ruleStack }
686
+ );
687
+ }
688
+
689
+ // src/lexer.js
332
690
  var DEFAULT_MODE = "default";
333
691
  function modesOf(def) {
334
692
  if (Array.isArray(def.modes) && def.modes.length > 0) return def.modes;
@@ -338,6 +696,8 @@ function modesOf(def) {
338
696
  function createLexer(defs, options = {}) {
339
697
  const positions = options.positions === "offset" ? "offset" : "full";
340
698
  const trackLines = positions === "full";
699
+ const useScanners = options.scanners !== false;
700
+ const useFuse = options.fuse !== false;
341
701
  if (!Array.isArray(defs) || defs.length === 0) throw new Error("createLexer: expected a non-empty array of token definitions");
342
702
  const tokenNames = defs.map((d) => d.name);
343
703
  const seen = /* @__PURE__ */ new Set();
@@ -357,7 +717,7 @@ function createLexer(defs, options = {}) {
357
717
  return id;
358
718
  };
359
719
  for (const def of defs) for (const mode of modesOf(def)) idOf(mode);
360
- const compiled = defs.map((def) => {
720
+ const compiled = defs.map((def, id) => {
361
721
  if (!def.name) throw new Error("createLexer: every token definition needs a name");
362
722
  if (!def.pattern && !def.literal) throw new Error(`createLexer: token '${def.name}' needs a pattern or a literal`);
363
723
  if (def.pattern && def.literal) throw new Error(`createLexer: token '${def.name}' has both a pattern and a literal; pick one`);
@@ -372,13 +732,24 @@ function createLexer(defs, options = {}) {
372
732
  }
373
733
  return {
374
734
  name: def.name,
735
+ // The declaration index. `createParser` derives the identical mapping
736
+ // from `tokenNames` independently (same array, same order), so a
737
+ // combinator that resolves a `consume` target's id at build time agrees
738
+ // with what a token actually carries without the two ever comparing notes.
739
+ id,
375
740
  skip: !!def.skip,
376
741
  literal: def.literal,
377
742
  longerAlt: def.longerAlt || null,
378
743
  regex: def.pattern ? new RegExp(def.pattern, def.ignoreCase ? "iy" : "y") : null,
744
+ // A table-driven stand-in for the regex where its shape is recognised;
745
+ // null otherwise (and always, with `scanners: false`). It is a field on
746
+ // every definition so they all keep one shape. See scan.js.
747
+ scanner: useScanners && def.pattern ? buildScanner(def.pattern, !!def.ignoreCase) : null,
379
748
  modes: modesOf(def),
380
749
  pushMode: def.pushMode ? idOf(def.pushMode) : -1,
381
- popMode: !!def.popMode
750
+ popMode: !!def.popMode,
751
+ // Set only on the stand-in `fuse` builds for a run of regexes; see there.
752
+ members: null
382
753
  };
383
754
  });
384
755
  const byName = new Map(compiled.map((c) => [c.name, c]));
@@ -395,14 +766,47 @@ function createLexer(defs, options = {}) {
395
766
  for (const c of compiled) {
396
767
  c.firstChars = c.literal !== void 0 ? c.literal.length > 0 ? /* @__PURE__ */ new Set([c.literal.charCodeAt(0)]) : null : firstCharSet(c.regex.source, !!c.regex.ignoreCase);
397
768
  }
398
- const modes = modeNames.map((name) => {
769
+ const FUSE_MIN = 3;
770
+ const fusible = (def) => def.literal === void 0 && def.scanner === null && def.regex !== null && !/\\[1-9]|\\k</.test(def.regex.source) && // `exec` of an always-matching `|` alternative returns one slot per capture group.
771
+ new RegExp(def.regex.source + "|").exec("").length === 1;
772
+ function fuse(list) {
773
+ if (!useFuse) return list;
774
+ const out = [];
775
+ for (let i = 0; i < list.length; ) {
776
+ let j = i;
777
+ while (j < list.length && fusible(list[j]) && list[j].regex.flags === list[i].regex.flags) j++;
778
+ if (j - i < FUSE_MIN) {
779
+ out.push(list[i++]);
780
+ continue;
781
+ }
782
+ const run = list.slice(i, j);
783
+ out.push({
784
+ name: run.map((d) => d.name).join("|"),
785
+ id: -1,
786
+ skip: false,
787
+ literal: void 0,
788
+ longerAlt: null,
789
+ longerAltDef: void 0,
790
+ regex: new RegExp(run.map((d) => `(${d.regex.source})`).join("|"), run[0].regex.flags),
791
+ scanner: null,
792
+ modes: run[0].modes,
793
+ pushMode: -1,
794
+ popMode: false,
795
+ members: run
796
+ });
797
+ i = j;
798
+ }
799
+ return out;
800
+ }
801
+ const modes = modeNames.map((name, id) => {
399
802
  const members = compiled.filter((c) => c.modes.includes(name));
400
803
  const alwaysTry = members.filter((c) => c.firstChars === null);
401
- const candidatesFor = (code) => members.filter((c) => c.firstChars === null || c.firstChars.has(code));
804
+ const candidatesFor = (code) => fuse(members.filter((c) => c.firstChars === null || c.firstChars.has(code)));
402
805
  const ascii = new Array(128);
403
806
  for (let code = 0; code < 128; code++) ascii[code] = candidatesFor(code);
404
807
  return {
405
808
  name,
809
+ id,
406
810
  members,
407
811
  ascii,
408
812
  wide: /* @__PURE__ */ new Map(),
@@ -426,6 +830,30 @@ function createLexer(defs, options = {}) {
426
830
  const multiMode = modes.length > 1 || compiled.some((c) => c.popMode || c.pushMode !== -1);
427
831
  const namesModes = modes.length > 1;
428
832
  const initial = modes[0];
833
+ let fusedMember = null;
834
+ let fusedText = "";
835
+ function fusedEnd(run, text, offset) {
836
+ run.regex.lastIndex = offset;
837
+ const match = run.regex.exec(text);
838
+ if (match === null) return -1;
839
+ if (match[0].length === 0) {
840
+ for (const member of run.members) {
841
+ member.regex.lastIndex = offset;
842
+ const one = member.regex.exec(text);
843
+ if (one !== null && one[0].length > 0) {
844
+ fusedMember = member;
845
+ fusedText = one[0];
846
+ return offset + fusedText.length;
847
+ }
848
+ }
849
+ return -1;
850
+ }
851
+ let group = 1;
852
+ while (match[group] === void 0) group++;
853
+ fusedMember = run.members[group - 1];
854
+ fusedText = match[0];
855
+ return offset + fusedText.length;
856
+ }
429
857
  function longerAltEnd(def, text, offset) {
430
858
  const alt2 = def.longerAltDef;
431
859
  if (alt2.literal !== void 0) return text.startsWith(alt2.literal, offset) ? offset + alt2.literal.length : -1;
@@ -445,19 +873,30 @@ function createLexer(defs, options = {}) {
445
873
  outer: while (offset < length) {
446
874
  const applicable = mode.dispatch ? candidates(mode, text.charCodeAt(offset)) : mode.members;
447
875
  for (let i = 0; i < applicable.length; i++) {
448
- const def = applicable[i];
876
+ let def = applicable[i];
449
877
  let end = -1;
450
878
  let matched = null;
451
- if (def.literal !== void 0) {
879
+ if (def.members !== null) {
880
+ end = fusedEnd(def, text, offset);
881
+ if (end === -1) continue;
882
+ def = fusedMember;
883
+ matched = fusedText;
884
+ } else if (def.literal !== void 0) {
452
885
  if (text.startsWith(def.literal, offset)) end = offset + def.literal.length;
453
- } else if (def.skip) {
454
- def.regex.lastIndex = offset;
455
- if (def.regex.test(text) && def.regex.lastIndex > offset) end = def.regex.lastIndex;
456
886
  } else {
457
- def.regex.lastIndex = offset;
458
- const match = def.regex.exec(text);
459
- if (match !== null && match[0].length > 0) matched = match[0];
460
- if (matched !== null) end = offset + matched.length;
887
+ end = def.scanner === null ? DEFER : scanEnd(def.scanner, text, offset, length);
888
+ if (end === DEFER) {
889
+ end = -1;
890
+ if (def.skip) {
891
+ def.regex.lastIndex = offset;
892
+ if (def.regex.test(text) && def.regex.lastIndex > offset) end = def.regex.lastIndex;
893
+ } else {
894
+ def.regex.lastIndex = offset;
895
+ const match = def.regex.exec(text);
896
+ if (match !== null && match[0].length > 0) matched = match[0];
897
+ if (matched !== null) end = offset + matched.length;
898
+ }
899
+ }
461
900
  }
462
901
  if (end === -1) continue;
463
902
  let winner = def;
@@ -470,14 +909,7 @@ function createLexer(defs, options = {}) {
470
909
  }
471
910
  if (!winner.skip) {
472
911
  const image = winner.literal !== void 0 ? winner.literal : winner === def && matched !== null ? matched : text.slice(offset, end);
473
- tokens.push({
474
- type: winner.name,
475
- image,
476
- start: offset,
477
- end,
478
- line: trackLines ? line : 0,
479
- col: trackLines ? offset - lineStart + 1 : 0
480
- });
912
+ tokens.push(trackLines ? { type: winner.name, image, start: offset, end, line, col: offset - lineStart + 1 } : { type: winner.name, id: winner.id, image, start: offset, end });
481
913
  }
482
914
  if (trackLines) {
483
915
  for (let j = offset; j < end; j++) {
@@ -513,62 +945,238 @@ function createLexer(defs, options = {}) {
513
945
  }
514
946
  const inMode = (mode) => namesModes ? ` (in lexer mode '${mode.name}')` : "";
515
947
  const describe = (offset, line, lineStart) => trackLines ? `line ${line} column ${offset - lineStart + 1}` : `offset ${offset}`;
516
- return { tokenize, tokenNames, positions, modeNames };
517
- }
518
-
519
- // src/state.js
520
- var FAIL = /* @__PURE__ */ Symbol("grammatik.fail");
521
- var createState = (tokens, grammar, options = {}) => ({
522
- tokens,
523
- idx: 0,
524
- maxIdx: 0,
525
- expected: [],
526
- grammar,
527
- memo: options.memo ? /* @__PURE__ */ new Map() : null,
528
- // Entering a rule is one array store rather than an object allocation, and
529
- // the stack is snapshotted only on a furthest failure — by the time an error
530
- // is built it would otherwise have unwound to just the start rule.
531
- ruleNames: [],
532
- ruleDepth: 0,
533
- failureStack: null,
534
- track: options.track === true,
535
- steps: 0,
536
- maxSteps: options.maxSteps || 0
537
- });
538
- function expect(state, expectation) {
539
- if (state.idx > state.maxIdx) {
540
- state.maxIdx = state.idx;
541
- if (!state.track) return;
542
- state.expected = [];
543
- state.failureStack = state.ruleNames.slice(0, state.ruleDepth);
948
+ if (!options.incremental) return { tokenize, tokenNames, positions, modeNames };
949
+ const recover = !!options.recover;
950
+ const errorTokenName = options.errorToken || "Error";
951
+ function snapshotStack(stack, depth) {
952
+ const ids = new Array(depth);
953
+ for (let i = 0; i < depth; i++) ids[i] = stack[i].id;
954
+ return ids;
544
955
  }
545
- if (!state.track || state.idx !== state.maxIdx) return;
546
- if (!state.expected.includes(expectation)) state.expected.push(expectation);
547
- }
548
- var ParseError = class extends Error {
549
- /**
550
- * @param {string} message
551
- * @param {{ token: import('./lexer.js').Token | null, expected: string[], ruleStack: string[] }} details
552
- */
553
- constructor(message, details) {
554
- super(message);
555
- this.name = "ParseError";
556
- this.token = details.token;
557
- this.expected = details.expected;
558
- this.ruleStack = details.ruleStack;
956
+ function sameStack(a, b) {
957
+ if (a.length !== b.length) return false;
958
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
959
+ return true;
559
960
  }
560
- };
561
- var describeExpected = (expected) => expected.length === 0 ? "nothing" : expected.length === 1 ? expected[0] : `one of [${expected.join(", ")}]`;
562
- function buildParseError(state) {
563
- const token = state.tokens[state.maxIdx] || null;
564
- const expected = [...state.expected].sort();
565
- const ruleStack = state.failureStack || state.ruleNames.slice(0, state.ruleDepth);
566
- const at = (t) => state.positions === "offset" ? `offset ${t.start}` : `line ${t.line} column ${t.col}`;
567
- const found = token ? `--> ${token.image} <-- (${token.type}) at ${at(token)}` : "end of input";
568
- return new ParseError(
569
- `Expecting ${describeExpected(expected)} but found ${found}`,
570
- { token, expected, ruleStack }
571
- );
961
+ function scan(text, offset, mode, stack, depth, line, lineStart) {
962
+ const tokens = [];
963
+ const checkpoints = [];
964
+ const length = text.length;
965
+ outer: while (offset < length) {
966
+ checkpoints.push({ modeId: mode.id, stack: snapshotStack(stack, depth), line, lineStart, offset });
967
+ const applicable = mode.dispatch ? candidates(mode, text.charCodeAt(offset)) : mode.members;
968
+ for (let i = 0; i < applicable.length; i++) {
969
+ let def = applicable[i];
970
+ let end = -1;
971
+ let matched = null;
972
+ if (def.members !== null) {
973
+ end = fusedEnd(def, text, offset);
974
+ if (end === -1) continue;
975
+ def = fusedMember;
976
+ matched = fusedText;
977
+ } else if (def.literal !== void 0) {
978
+ if (text.startsWith(def.literal, offset)) end = offset + def.literal.length;
979
+ } else {
980
+ end = def.scanner === null ? DEFER : scanEnd(def.scanner, text, offset, length);
981
+ if (end === DEFER) {
982
+ end = -1;
983
+ if (def.skip) {
984
+ def.regex.lastIndex = offset;
985
+ if (def.regex.test(text) && def.regex.lastIndex > offset) end = def.regex.lastIndex;
986
+ } else {
987
+ def.regex.lastIndex = offset;
988
+ const match = def.regex.exec(text);
989
+ if (match !== null && match[0].length > 0) matched = match[0];
990
+ if (matched !== null) end = offset + matched.length;
991
+ }
992
+ }
993
+ }
994
+ if (end === -1) continue;
995
+ let winner = def;
996
+ if (def.longerAltDef) {
997
+ const altEnd = longerAltEnd(def, text, offset);
998
+ if (altEnd > end) {
999
+ end = altEnd;
1000
+ winner = def.longerAltDef;
1001
+ }
1002
+ }
1003
+ if (!winner.skip) {
1004
+ const image = winner.literal !== void 0 ? winner.literal : winner === def && matched !== null ? matched : text.slice(offset, end);
1005
+ tokens.push(trackLines ? { type: winner.name, image, start: offset, end, line, col: offset - lineStart + 1 } : { type: winner.name, id: winner.id, image, start: offset, end });
1006
+ }
1007
+ if (trackLines) {
1008
+ for (let j = offset; j < end; j++) {
1009
+ if (text.charCodeAt(j) !== 10) continue;
1010
+ line++;
1011
+ lineStart = j + 1;
1012
+ }
1013
+ }
1014
+ if (multiMode) {
1015
+ if (winner.popMode) {
1016
+ if (depth === 0) {
1017
+ if (!recover) {
1018
+ throw new LexError(
1019
+ `Token '${winner.name}' popped the initial lexer mode at ${describe(offset, line, lineStart)}`,
1020
+ { offset, line: trackLines ? line : 0, col: trackLines ? offset - lineStart + 1 : 0 }
1021
+ );
1022
+ }
1023
+ } else {
1024
+ mode = stack[--depth];
1025
+ }
1026
+ } else if (winner.pushMode !== -1) {
1027
+ stack[depth++] = mode;
1028
+ mode = modes[winner.pushMode];
1029
+ }
1030
+ }
1031
+ offset = end;
1032
+ continue outer;
1033
+ }
1034
+ if (recover) {
1035
+ tokens.push(trackLines ? { type: errorTokenName, image: text[offset], start: offset, end: offset + 1, line, col: offset - lineStart + 1 } : { type: errorTokenName, id: -1, image: text[offset], start: offset, end: offset + 1 });
1036
+ if (trackLines && text.charCodeAt(offset) === 10) {
1037
+ line++;
1038
+ lineStart = offset + 1;
1039
+ }
1040
+ offset++;
1041
+ continue outer;
1042
+ }
1043
+ const column = offset - lineStart + 1;
1044
+ throw new LexError(
1045
+ trackLines ? `Unexpected character '${text[offset]}' at line ${line} column ${column}${inMode(mode)}` : `Unexpected character '${text[offset]}' at offset ${offset}${inMode(mode)}`,
1046
+ { offset, line: trackLines ? line : 0, col: trackLines ? column : 0 }
1047
+ );
1048
+ }
1049
+ return { tokens, checkpoints, mode, stack, depth, line, lineStart };
1050
+ }
1051
+ function tokenizeIncremental(text) {
1052
+ if (typeof text !== "string") throw new TypeError("tokenizeIncremental: expected a string");
1053
+ const result = scan(text, 0, initial, multiMode ? [] : [], 0, 1, 0);
1054
+ return { text, tokens: result.tokens, checkpoints: result.checkpoints, eof: { modeId: result.mode.id, stack: snapshotStack(result.stack, result.depth), line: result.line, lineStart: result.lineStart, offset: text.length } };
1055
+ }
1056
+ function tokenFloor(tokens, offset) {
1057
+ let lo = 0;
1058
+ let hi = tokens.length;
1059
+ while (lo < hi) {
1060
+ const mid = lo + hi >>> 1;
1061
+ if (tokens[mid].start < offset) lo = mid + 1;
1062
+ else hi = mid;
1063
+ }
1064
+ return lo;
1065
+ }
1066
+ function checkpointFloor(checkpoints, eof, offset) {
1067
+ let lo = 0;
1068
+ let hi = checkpoints.length + 1;
1069
+ while (lo < hi) {
1070
+ const mid = lo + hi >>> 1;
1071
+ const at = mid < checkpoints.length ? checkpoints[mid].offset : eof.offset;
1072
+ if (at <= offset) lo = mid + 1;
1073
+ else hi = mid;
1074
+ }
1075
+ return lo - 1;
1076
+ }
1077
+ function checkpointAt(checkpoints, eof, offset) {
1078
+ if (eof.offset === offset) return eof;
1079
+ let lo = 0;
1080
+ let hi = checkpoints.length;
1081
+ while (lo < hi) {
1082
+ const mid = lo + hi >>> 1;
1083
+ if (checkpoints[mid].offset < offset) lo = mid + 1;
1084
+ else hi = mid;
1085
+ }
1086
+ return lo < checkpoints.length && checkpoints[lo].offset === offset ? checkpoints[lo] : null;
1087
+ }
1088
+ function relex(newText, prev, edit) {
1089
+ if (typeof newText !== "string") throw new TypeError("relex: expected a string");
1090
+ const tokens = prev.tokens;
1091
+ const checkpoints = prev.checkpoints;
1092
+ const delta = edit.newEnd - edit.oldEnd;
1093
+ let resumeIndex = checkpointFloor(checkpoints, prev.eof, edit.start);
1094
+ if (resumeIndex > 0) resumeIndex--;
1095
+ let boundary = resumeIndex < checkpoints.length ? checkpoints[resumeIndex].offset : prev.eof.offset;
1096
+ let ti = tokenFloor(tokens, boundary);
1097
+ while (ti > 0 && tokens[ti - 1].end === boundary) {
1098
+ ti--;
1099
+ boundary = tokens[ti].start;
1100
+ }
1101
+ if (boundary < (resumeIndex < checkpoints.length ? checkpoints[resumeIndex].offset : prev.eof.offset)) {
1102
+ resumeIndex = checkpointFloor(checkpoints, prev.eof, boundary);
1103
+ }
1104
+ const resume = resumeIndex < checkpoints.length ? checkpoints[resumeIndex] : prev.eof;
1105
+ const resumeMode = modes[resume.modeId];
1106
+ const resumeStack = resume.stack.map((id) => modes[id]);
1107
+ const scanned = scan(newText, resume.offset, resumeMode, resumeStack, resumeStack.length, resume.line, resume.lineStart);
1108
+ const scannedEof = { modeId: scanned.mode.id, stack: snapshotStack(scanned.stack, scanned.depth), line: scanned.line, lineStart: scanned.lineStart, offset: newText.length };
1109
+ const VERIFY_WINDOW = 4;
1110
+ function convergenceVerified(oldOffset, newOffset) {
1111
+ const oldStart = tokenFloor(tokens, oldOffset);
1112
+ const freshStart = tokenFloor(scanned.tokens, newOffset);
1113
+ for (let k = 0; k < VERIFY_WINDOW; k++) {
1114
+ const oldTok = tokens[oldStart + k];
1115
+ const freshTok = scanned.tokens[freshStart + k];
1116
+ if (oldTok === void 0 && freshTok === void 0) return true;
1117
+ if (oldTok === void 0 || freshTok === void 0) return false;
1118
+ if (oldTok.type !== freshTok.type || oldTok.image !== freshTok.image) return false;
1119
+ }
1120
+ return true;
1121
+ }
1122
+ let convergeNewOffset = -1;
1123
+ let convergeOldOffset = -1;
1124
+ let convergeOldCheckpoint = null;
1125
+ let freshCheckpointCount = 0;
1126
+ for (let s = 0; s <= scanned.checkpoints.length; s++) {
1127
+ const cp = s < scanned.checkpoints.length ? scanned.checkpoints[s] : scannedEof;
1128
+ if (cp.offset < edit.newEnd) continue;
1129
+ const oldOffset = cp.offset - delta;
1130
+ const oldCp = checkpointAt(checkpoints, prev.eof, oldOffset);
1131
+ if (oldCp && oldCp.modeId === cp.modeId && sameStack(oldCp.stack, cp.stack) && convergenceVerified(oldOffset, cp.offset)) {
1132
+ convergeNewOffset = cp.offset;
1133
+ convergeOldOffset = oldOffset;
1134
+ convergeOldCheckpoint = oldCp;
1135
+ freshCheckpointCount = s;
1136
+ break;
1137
+ }
1138
+ }
1139
+ const tokenFrom = tokenFloor(tokens, resume.offset);
1140
+ const checkpointFrom = resumeIndex;
1141
+ if (convergeNewOffset === -1) {
1142
+ tokens.splice(tokenFrom, tokens.length - tokenFrom, ...scanned.tokens);
1143
+ checkpoints.splice(checkpointFrom, checkpoints.length - checkpointFrom, ...scanned.checkpoints);
1144
+ prev.text = newText;
1145
+ prev.eof = scannedEof;
1146
+ return prev;
1147
+ }
1148
+ const freshTokenCount = tokenFloor(scanned.tokens, convergeNewOffset);
1149
+ const lineDelta = trackLines ? scanned.checkpoints.length > freshCheckpointCount ? scanned.checkpoints[freshCheckpointCount].line - convergeOldCheckpoint.line : scannedEof.line - convergeOldCheckpoint.line : 0;
1150
+ const boundaryOldLineStart = convergeOldCheckpoint.lineStart;
1151
+ const boundaryNewLineStart = (freshCheckpointCount < scanned.checkpoints.length ? scanned.checkpoints[freshCheckpointCount] : scannedEof).lineStart;
1152
+ const shiftedLineStart = (oldLineStart) => oldLineStart === boundaryOldLineStart ? boundaryNewLineStart : oldLineStart + delta;
1153
+ const tailTokenFrom = tokenFloor(tokens, convergeOldOffset);
1154
+ for (let i = tailTokenFrom; i < tokens.length; i++) {
1155
+ const t = tokens[i];
1156
+ if (trackLines) {
1157
+ const oldLineStart = t.start - (t.col - 1);
1158
+ t.col = t.start + delta - shiftedLineStart(oldLineStart) + 1;
1159
+ t.line += lineDelta;
1160
+ }
1161
+ t.start += delta;
1162
+ t.end += delta;
1163
+ }
1164
+ tokens.splice(tokenFrom, tailTokenFrom - tokenFrom, ...scanned.tokens.slice(0, freshTokenCount));
1165
+ const tailCheckpointFrom = checkpointFloor(checkpoints, prev.eof, convergeOldOffset - 1) + 1;
1166
+ for (let i = tailCheckpointFrom; i < checkpoints.length; i++) {
1167
+ const cp = checkpoints[i];
1168
+ cp.lineStart = shiftedLineStart(cp.lineStart);
1169
+ cp.line += lineDelta;
1170
+ cp.offset += delta;
1171
+ }
1172
+ checkpoints.splice(checkpointFrom, tailCheckpointFrom - checkpointFrom, ...scanned.checkpoints.slice(0, freshCheckpointCount));
1173
+ prev.text = newText;
1174
+ prev.eof.lineStart = shiftedLineStart(prev.eof.lineStart);
1175
+ prev.eof.line += lineDelta;
1176
+ prev.eof.offset += delta;
1177
+ return prev;
1178
+ }
1179
+ return { tokenize, tokenizeIncremental, relex, tokenNames, positions, modeNames };
572
1180
  }
573
1181
 
574
1182
  // src/analyze.js
@@ -601,6 +1209,7 @@ var PARSER_OPS = /* @__PURE__ */ new Set([
601
1209
  "many",
602
1210
  "many1",
603
1211
  "option",
1212
+ "flag",
604
1213
  "manySep",
605
1214
  "many1Sep",
606
1215
  "infixLeft",
@@ -608,6 +1217,8 @@ var PARSER_OPS = /* @__PURE__ */ new Set([
608
1217
  "postfix",
609
1218
  "prefix",
610
1219
  "between",
1220
+ "pick",
1221
+ "yields",
611
1222
  "as",
612
1223
  "oneOf",
613
1224
  "eof",
@@ -661,7 +1272,8 @@ function firstOf(node, ruleFirsts) {
661
1272
  return result;
662
1273
  }
663
1274
  case "many":
664
- case "option": {
1275
+ case "option":
1276
+ case "flag": {
665
1277
  const inner = firstOf(arg, ruleFirsts);
666
1278
  return { tokens: inner.tokens, nullable: true, unknown: inner.unknown };
667
1279
  }
@@ -684,6 +1296,9 @@ function firstOf(node, ruleFirsts) {
684
1296
  return firstOf(Array.isArray(arg) ? arg[0] : arg && arg.parser, ruleFirsts);
685
1297
  case "between":
686
1298
  return firstOf({ seq: [arg && arg.open, arg && arg.body, arg && arg.close] }, ruleFirsts);
1299
+ case "pick":
1300
+ case "yields":
1301
+ return firstOf({ seq: arg && Array.isArray(arg.parsers) ? arg.parsers : [] }, ruleFirsts);
687
1302
  case "prefix": {
688
1303
  const ops = firstOf(arg && arg.operator, ruleFirsts);
689
1304
  const inner = firstOf(arg && arg.operand, ruleFirsts);
@@ -747,12 +1362,94 @@ function recognizeSeqIdiom(children) {
747
1362
  if (rewritten !== BAIL) return { as: [parser, rewritten] };
748
1363
  }
749
1364
  }
1365
+ if (children.length >= 2) {
1366
+ const last = children[children.length - 1];
1367
+ const head = children.slice(0, -1);
1368
+ if (opOf(last) === "action" && isConstant(last.action) && head.every(isPlainParser) && !head.some(usesOuterScope)) {
1369
+ return { yields: { parsers: head, value: last.action } };
1370
+ }
1371
+ if (opOf(last) === "action" && opOf(last.action) === "val" && typeof last.action.val === "string") {
1372
+ const name = last.action.val;
1373
+ const labelled = head.map((child, i) => opOf(child) === "label" ? i : -1).filter((i) => i !== -1);
1374
+ if (labelled.length === 1 && head[labelled[0]].label[0] === name && head.every((child, i) => i === labelled[0] || isPlainParser(child)) && !head.some(usesOuterScope)) {
1375
+ return { pick: { parsers: head.map((child, i) => i === labelled[0] ? child.label[1] : child), index: labelled[0] } };
1376
+ }
1377
+ }
1378
+ }
750
1379
  return null;
751
1380
  }
1381
+ var isConstant = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1382
+ function usesOuterScope(logic) {
1383
+ if (Array.isArray(logic)) return logic.some(usesOuterScope);
1384
+ if (!logic || typeof logic !== "object") return false;
1385
+ for (const key of Object.keys(logic)) {
1386
+ const arg = logic[key];
1387
+ if (key === "val" && Array.isArray(arg) && Array.isArray(arg[0])) return true;
1388
+ if (key === "var") {
1389
+ if (typeof arg === "string" && arg.includes("../")) return true;
1390
+ if (Array.isArray(arg) && typeof arg[0] === "string" && arg[0].includes("../")) return true;
1391
+ }
1392
+ if (usesOuterScope(arg)) return true;
1393
+ }
1394
+ return false;
1395
+ }
752
1396
  function isPlainParser(node) {
753
1397
  const op = opOf(node);
754
1398
  return op !== null && op !== "label" && op !== "action";
755
1399
  }
1400
+ function deepEqual(a, b) {
1401
+ if (a === b) return true;
1402
+ if (Array.isArray(a) || Array.isArray(b)) {
1403
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
1404
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
1405
+ return true;
1406
+ }
1407
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
1408
+ const keysA = Object.keys(a);
1409
+ const keysB = Object.keys(b);
1410
+ if (keysA.length !== keysB.length) return false;
1411
+ for (const key of keysA) {
1412
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false;
1413
+ }
1414
+ return true;
1415
+ }
1416
+ function recognizeAltPrefixIdiom(branches) {
1417
+ if (!Array.isArray(branches) || branches.length < 2) return null;
1418
+ if (!branches.every((b) => opOf(b) === "seq" && Array.isArray(b.seq))) return null;
1419
+ const seqs = branches.map((b) => b.seq);
1420
+ const minLen = Math.min(...seqs.map((s) => s.length));
1421
+ let n = 0;
1422
+ while (n < minLen) {
1423
+ const candidate = seqs[0][n];
1424
+ const op = opOf(candidate);
1425
+ if (op === "label" || op === "action") break;
1426
+ if (!seqs.every((s) => deepEqual(s[n], candidate))) break;
1427
+ n++;
1428
+ }
1429
+ if (n === 0) return null;
1430
+ if (!seqs.every((s) => s.slice(n).some((child) => opOf(child) === "action"))) return null;
1431
+ const prefix = seqs[0].slice(0, n);
1432
+ const remainders = seqs.map((s) => ({ seq: s.slice(n) }));
1433
+ return {
1434
+ seq: [
1435
+ ...prefix,
1436
+ { label: ["__grammatik_rest", { alt: remainders }] },
1437
+ { action: { val: "__grammatik_rest" } }
1438
+ ]
1439
+ };
1440
+ }
1441
+ function recognizeAltConsumeIdiom(branches) {
1442
+ if (!Array.isArray(branches) || branches.length < 2) return null;
1443
+ const names = [];
1444
+ for (const branch of branches) {
1445
+ if (opOf(branch) !== "consume") return null;
1446
+ const arg = branch.consume;
1447
+ const name = typeof arg === "string" ? arg : Array.isArray(arg) && arg.length === 1 ? arg[0] : null;
1448
+ if (typeof name !== "string") return null;
1449
+ names.push(name);
1450
+ }
1451
+ return { oneOf: names };
1452
+ }
756
1453
  var BAIL = /* @__PURE__ */ Symbol("grammatik.bail");
757
1454
  function rebaseOnLabel(logic, name) {
758
1455
  if (Array.isArray(logic)) {
@@ -819,6 +1516,7 @@ function leftmostCalls(node, ruleFirsts, out) {
819
1516
  case "many":
820
1517
  case "many1":
821
1518
  case "option":
1519
+ case "flag":
822
1520
  case "lookahead":
823
1521
  case "negLookahead":
824
1522
  leftmostCalls(arg, ruleFirsts, out);
@@ -829,6 +1527,10 @@ function leftmostCalls(node, ruleFirsts, out) {
829
1527
  case "between":
830
1528
  if (arg) leftmostCalls({ seq: [arg.open, arg.body, arg.close] }, ruleFirsts, out);
831
1529
  break;
1530
+ case "pick":
1531
+ case "yields":
1532
+ if (arg && Array.isArray(arg.parsers)) leftmostCalls({ seq: arg.parsers }, ruleFirsts, out);
1533
+ break;
832
1534
  case "prefix":
833
1535
  if (arg) {
834
1536
  leftmostCalls(arg.operator, ruleFirsts, out);
@@ -895,6 +1597,7 @@ function validateNode(node, grammar, tokenNames, errors, path) {
895
1597
  case "many":
896
1598
  case "many1":
897
1599
  case "option":
1600
+ case "flag":
898
1601
  case "text":
899
1602
  case "lookahead":
900
1603
  case "negLookahead":
@@ -916,6 +1619,26 @@ function validateNode(node, grammar, tokenNames, errors, path) {
916
1619
  }
917
1620
  break;
918
1621
  }
1622
+ case "pick": {
1623
+ if (!arg || !Array.isArray(arg.parsers) || arg.parsers.length === 0) {
1624
+ errors.push(`${path}: 'pick' expects { parsers: [parser, ...], index }`);
1625
+ } else if (!Number.isInteger(arg.index) || arg.index < 0 || arg.index >= arg.parsers.length) {
1626
+ errors.push(`${path}: 'pick' index ${JSON.stringify(arg.index)} is out of range for ${arg.parsers.length} parser(s)`);
1627
+ } else {
1628
+ arg.parsers.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.pick[${i}]`));
1629
+ }
1630
+ break;
1631
+ }
1632
+ case "yields": {
1633
+ if (!arg || !Array.isArray(arg.parsers) || arg.parsers.length === 0 || !("value" in arg)) {
1634
+ errors.push(`${path}: 'yields' expects { parsers: [parser, ...], value }`);
1635
+ } else if (!isConstant(arg.value)) {
1636
+ errors.push(`${path}: 'yields' value must be a string, number, boolean or null, got ${JSON.stringify(arg.value)}`);
1637
+ } else {
1638
+ arg.parsers.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.yields[${i}]`));
1639
+ }
1640
+ break;
1641
+ }
919
1642
  case "prefix": {
920
1643
  if (!arg || typeof arg !== "object" || !("operator" in arg) || !("operand" in arg) || !("combine" in arg)) {
921
1644
  errors.push(`${path}: 'prefix' expects { operator, operand, combine }`);
@@ -1002,16 +1725,38 @@ function analyze(grammar, options = {}) {
1002
1725
  return { firsts, errors, leftRecursive };
1003
1726
  }
1004
1727
  function buildDispatch(branches, firsts) {
1005
- const table = /* @__PURE__ */ new Map();
1006
- for (const branch of branches) {
1007
- const first = firstOf(branch, firsts);
1728
+ const perBranch = branches.map((branch) => firstOf(branch, firsts));
1729
+ for (const first of perBranch) {
1008
1730
  if (first.nullable || first.unknown || first.tokens.size === 0) return null;
1009
- for (const token of first.tokens) {
1010
- if (table.has(token)) return null;
1011
- table.set(token, branch);
1731
+ }
1732
+ const byToken = /* @__PURE__ */ new Map();
1733
+ for (let i = 0; i < branches.length; i++) {
1734
+ for (const token of perBranch[i].tokens) {
1735
+ let list = byToken.get(token);
1736
+ if (list === void 0) {
1737
+ list = [];
1738
+ byToken.set(token, list);
1739
+ }
1740
+ list.push(branches[i]);
1741
+ }
1742
+ }
1743
+ if (byToken.size === 0) return null;
1744
+ const canonical = /* @__PURE__ */ new Map();
1745
+ const table = /* @__PURE__ */ new Map();
1746
+ for (const [token, list] of byToken) {
1747
+ if (list.length === 1) {
1748
+ table.set(token, list[0]);
1749
+ continue;
1750
+ }
1751
+ const key = list.map((branch) => branches.indexOf(branch)).join(",");
1752
+ let group = canonical.get(key);
1753
+ if (group === void 0) {
1754
+ group = list;
1755
+ canonical.set(key, group);
1012
1756
  }
1757
+ table.set(token, group);
1013
1758
  }
1014
- return table.size > 0 ? table : null;
1759
+ return table;
1015
1760
  }
1016
1761
 
1017
1762
  // src/plan.js
@@ -1020,20 +1765,6 @@ function declareSync(fn) {
1020
1765
  fn[SYNC] = true;
1021
1766
  return fn;
1022
1767
  }
1023
- function usesOuterScope(logic) {
1024
- if (Array.isArray(logic)) return logic.some(usesOuterScope);
1025
- if (!logic || typeof logic !== "object") return false;
1026
- for (const key of Object.keys(logic)) {
1027
- const arg = logic[key];
1028
- if (key === "val" && Array.isArray(arg) && Array.isArray(arg[0])) return true;
1029
- if (key === "var") {
1030
- if (typeof arg === "string" && arg.includes("../")) return true;
1031
- if (Array.isArray(arg) && typeof arg[0] === "string" && arg[0].includes("../")) return true;
1032
- }
1033
- if (usesOuterScope(arg)) return true;
1034
- }
1035
- return false;
1036
- }
1037
1768
  function mayReturnContext(logic) {
1038
1769
  if (Array.isArray(logic)) return logic.some(mayReturnContext);
1039
1770
  if (!logic || typeof logic !== "object") return false;
@@ -1046,7 +1777,7 @@ function mayReturnContext(logic) {
1046
1777
  return false;
1047
1778
  }
1048
1779
  function createPlanner(config) {
1049
- const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config;
1780
+ const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null, memoSlots = /* @__PURE__ */ new Map(), idioms = true } = config;
1050
1781
  const slots = /* @__PURE__ */ new Map();
1051
1782
  const planCache = /* @__PURE__ */ new WeakMap();
1052
1783
  const opCaches = /* @__PURE__ */ new Map();
@@ -1129,9 +1860,10 @@ function createPlanner(config) {
1129
1860
  }
1130
1861
  const buildConsume = (arg) => {
1131
1862
  const name = typeof arg === "string" ? arg : arg[0];
1863
+ const id = tokenIds ? tokenIds.get(name) : void 0;
1132
1864
  return (state) => {
1133
1865
  const token = state.tokens[state.idx];
1134
- if (token !== void 0 && token.type === name) {
1866
+ if (token !== void 0 && (token.id === id || token.id === void 0 && token.type === name)) {
1135
1867
  state.idx++;
1136
1868
  return token;
1137
1869
  }
@@ -1192,6 +1924,17 @@ function createPlanner(config) {
1192
1924
  return value;
1193
1925
  };
1194
1926
  };
1927
+ const buildFlag = (arg) => {
1928
+ const child = plan(arg);
1929
+ return (state) => {
1930
+ const start = state.idx;
1931
+ if (child(state) === FAIL) {
1932
+ state.idx = start;
1933
+ return false;
1934
+ }
1935
+ return true;
1936
+ };
1937
+ };
1195
1938
  const buildInfixLeft = (arg) => {
1196
1939
  const operand = plan(arg.operand);
1197
1940
  const operator = arg.operator === void 0 ? null : plan(arg.operator);
@@ -1227,10 +1970,11 @@ function createPlanner(config) {
1227
1970
  const buildOneOf = (arg) => {
1228
1971
  const names = Array.isArray(arg) ? arg : [arg];
1229
1972
  const set = new Set(names);
1973
+ const idSet = tokenIds ? new Set(names.map((n) => tokenIds.get(n))) : null;
1230
1974
  const count = names.length;
1231
1975
  return (state) => {
1232
1976
  const token = state.tokens[state.idx];
1233
- if (token !== void 0 && set.has(token.type)) {
1977
+ if (token !== void 0 && (idSet ? token.id !== void 0 ? idSet.has(token.id) : set.has(token.type) : set.has(token.type))) {
1234
1978
  state.idx++;
1235
1979
  return token;
1236
1980
  }
@@ -1316,6 +2060,26 @@ function createPlanner(config) {
1316
2060
  return value;
1317
2061
  };
1318
2062
  };
2063
+ const buildSequence = (arg, pickIndex, constant) => {
2064
+ const fns = arg.parsers.map(plan);
2065
+ const count = fns.length;
2066
+ return (state) => {
2067
+ tick(state);
2068
+ const start = state.idx;
2069
+ let picked;
2070
+ for (let i = 0; i < count; i++) {
2071
+ const value = fns[i](state);
2072
+ if (value === FAIL) {
2073
+ state.idx = start;
2074
+ return FAIL;
2075
+ }
2076
+ if (i === pickIndex) picked = value;
2077
+ }
2078
+ return pickIndex === -1 ? constant : picked;
2079
+ };
2080
+ };
2081
+ const buildPick = (arg) => buildSequence(arg, arg.index, void 0);
2082
+ const buildYields = (arg) => buildSequence(arg, -1, arg.value);
1319
2083
  const buildPrefix = (arg) => {
1320
2084
  const operator = plan(arg.operator);
1321
2085
  const operand = plan(arg.operand);
@@ -1399,6 +2163,31 @@ function createPlanner(config) {
1399
2163
  return value;
1400
2164
  };
1401
2165
  }
2166
+ const memoSlot = memoSlots.get(name);
2167
+ if (memoSlot !== void 0) {
2168
+ return (state) => {
2169
+ tick(state);
2170
+ const table = state.rm[memoSlot];
2171
+ const at = state.idx;
2172
+ const end = table.ends[at];
2173
+ if (end !== 0) {
2174
+ state.idx = end - 1;
2175
+ return table.vals[at];
2176
+ }
2177
+ let depth = 0;
2178
+ if (state.track) {
2179
+ depth = state.ruleDepth;
2180
+ state.ruleNames[depth] = name;
2181
+ state.ruleDepth = depth + 1;
2182
+ }
2183
+ const value = slot.fn(state);
2184
+ if (state.track) state.ruleDepth = depth;
2185
+ if (value === FAIL) state.idx = at;
2186
+ table.ends[at] = state.idx + 1;
2187
+ table.vals[at] = value;
2188
+ return value;
2189
+ };
2190
+ }
1402
2191
  return (state) => {
1403
2192
  tick(state);
1404
2193
  if (!state.track) return slot.fn(state);
@@ -1477,6 +2266,9 @@ function createPlanner(config) {
1477
2266
  infixRight: buildInfixRight,
1478
2267
  as: buildAs,
1479
2268
  between: buildBetween,
2269
+ flag: buildFlag,
2270
+ pick: buildPick,
2271
+ yields: buildYields,
1480
2272
  prefix: buildPrefix,
1481
2273
  postfix: buildPostfix,
1482
2274
  manySep: (arg) => planSeparated(arg, false),
@@ -1496,7 +2288,7 @@ function createPlanner(config) {
1496
2288
  }
1497
2289
  function planSeq(children) {
1498
2290
  if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers");
1499
- const idiom = recognizeSeqIdiom(children);
2291
+ const idiom = idioms ? recognizeSeqIdiom(children) : null;
1500
2292
  if (idiom !== null) return planUncached(idiom);
1501
2293
  const PARSER = 0;
1502
2294
  const LABELLED = 1;
@@ -1506,6 +2298,7 @@ function createPlanner(config) {
1506
2298
  const kinds = [];
1507
2299
  const names = [];
1508
2300
  const tokens = [];
2301
+ const tokenIdList = [];
1509
2302
  const fns = [];
1510
2303
  const actionLogic = [];
1511
2304
  let hasAction = false;
@@ -1526,11 +2319,13 @@ function createPlanner(config) {
1526
2319
  kinds.push(LABELLED_TERMINAL);
1527
2320
  names.push(name);
1528
2321
  tokens.push(terminal2);
2322
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal2) : void 0);
1529
2323
  fns.push(null);
1530
2324
  } else {
1531
2325
  kinds.push(LABELLED);
1532
2326
  names.push(name);
1533
2327
  tokens.push(null);
2328
+ tokenIdList.push(void 0);
1534
2329
  fns.push(plan(inner));
1535
2330
  }
1536
2331
  continue;
@@ -1539,6 +2334,7 @@ function createPlanner(config) {
1539
2334
  kinds.push(ACTION);
1540
2335
  names.push(null);
1541
2336
  tokens.push(null);
2337
+ tokenIdList.push(void 0);
1542
2338
  fns.push(planAction(child.action));
1543
2339
  actionLogic.push(child.action);
1544
2340
  hasAction = true;
@@ -1549,11 +2345,13 @@ function createPlanner(config) {
1549
2345
  kinds.push(TERMINAL);
1550
2346
  names.push(null);
1551
2347
  tokens.push(terminal);
2348
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal) : void 0);
1552
2349
  fns.push(null);
1553
2350
  } else {
1554
2351
  kinds.push(PARSER);
1555
2352
  names.push(null);
1556
2353
  tokens.push(null);
2354
+ tokenIdList.push(void 0);
1557
2355
  fns.push(plan(child));
1558
2356
  }
1559
2357
  }
@@ -1574,7 +2372,7 @@ function createPlanner(config) {
1574
2372
  let value;
1575
2373
  if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
1576
2374
  const token = state.tokens[state.idx];
1577
- if (token !== void 0 && token.type === tokens[i]) {
2375
+ if (token !== void 0 && (token.id === tokenIdList[i] || token.id === void 0 && token.type === tokens[i])) {
1578
2376
  state.idx++;
1579
2377
  value = token;
1580
2378
  } else {
@@ -1604,18 +2402,46 @@ function createPlanner(config) {
1604
2402
  }
1605
2403
  function planAlt(branches) {
1606
2404
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches");
2405
+ const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null;
2406
+ if (oneOf !== null) return planUncached(oneOf);
2407
+ const factored = idioms ? recognizeAltPrefixIdiom(branches) : null;
2408
+ if (factored !== null) return planUncached(factored);
1607
2409
  const planned = branches.map(plan);
1608
2410
  const table = firsts ? buildDispatch(branches, firsts) : null;
1609
2411
  if (table !== null) {
1610
2412
  const byToken = /* @__PURE__ */ Object.create(null);
1611
- for (const [token, branch] of table) byToken[token] = planned[branches.indexOf(branch)];
2413
+ const byId = tokenIds ? [] : null;
2414
+ const groupClosures = /* @__PURE__ */ new Map();
2415
+ for (const [token, branch] of table) {
2416
+ let closure = groupClosures.get(branch);
2417
+ if (closure === void 0) {
2418
+ closure = Array.isArray(branch) ? (() => {
2419
+ const group = branch.map((b) => planned[branches.indexOf(b)]);
2420
+ return (state) => {
2421
+ const start = state.idx;
2422
+ for (let i = 0; i < group.length; i++) {
2423
+ const value = group[i](state);
2424
+ if (value !== FAIL) return value;
2425
+ state.idx = start;
2426
+ }
2427
+ return FAIL;
2428
+ };
2429
+ })() : planned[branches.indexOf(branch)];
2430
+ groupClosures.set(branch, closure);
2431
+ }
2432
+ byToken[token] = closure;
2433
+ if (byId) {
2434
+ const id = tokenIds.get(token);
2435
+ if (id !== void 0) byId[id] = closure;
2436
+ }
2437
+ }
1612
2438
  const expectedTokens = [...table.keys()];
1613
2439
  const expectedCount = expectedTokens.length;
1614
2440
  return (state) => {
1615
2441
  tick(state);
1616
2442
  const token = state.tokens[state.idx];
1617
2443
  if (token !== void 0) {
1618
- const branch = byToken[token.type];
2444
+ const branch = byId && token.id !== void 0 ? byId[token.id] : byToken[token.type];
1619
2445
  if (branch !== void 0) return branch(state);
1620
2446
  }
1621
2447
  for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i]);
@@ -1700,6 +2526,9 @@ var OPERATORS = [
1700
2526
  "prefix",
1701
2527
  "postfix",
1702
2528
  "between",
2529
+ "pick",
2530
+ "yields",
2531
+ "flag",
1703
2532
  "as",
1704
2533
  "subrule",
1705
2534
  "label",
@@ -1754,7 +2583,12 @@ var createValueMethods = () => ({
1754
2583
 
1755
2584
  // src/codegen.js
1756
2585
  function createCodegen(config) {
1757
- const { planner, rules, firsts, memo, maxSteps } = config;
2586
+ const { planner, rules, firsts, memo, maxSteps, tokenIds = null, memoSlots = /* @__PURE__ */ new Map(), inlineRules = true, idioms = true } = config;
2587
+ const typeCheck = (tokenVar, name) => {
2588
+ const id = tokenIds ? tokenIds.get(name) : void 0;
2589
+ if (id === void 0) return `${tokenVar}.type === ${JSON.stringify(name)}`;
2590
+ return `(${tokenVar}.id === ${id} || (${tokenVar}.id === undefined && ${tokenVar}.type === ${JSON.stringify(name)}))`;
2591
+ };
1758
2592
  const HELPERS = [];
1759
2593
  const ORIGINS = [];
1760
2594
  const RULES = [];
@@ -1770,6 +2604,33 @@ function createCodegen(config) {
1770
2604
  }
1771
2605
  const actionHelper = (logic) => helper(planner.planAction(logic), { kind: "action", logic });
1772
2606
  const constantHelper = (node) => helper(planner.plan(node), { kind: "constant", node });
2607
+ const MAX_INLINE_DEPTH = 6;
2608
+ const ONE_OF_SWITCH_AT = 12;
2609
+ const INLINE_OPS = /* @__PURE__ */ new Set(["infixLeft", "infixRight", "prefix", "postfix"]);
2610
+ const callSites = /* @__PURE__ */ new Map();
2611
+ (function count(node) {
2612
+ if (Array.isArray(node)) {
2613
+ for (const child of node) count(child);
2614
+ return;
2615
+ }
2616
+ if (!node || typeof node !== "object") return;
2617
+ if ("subrule" in node) {
2618
+ const target = Array.isArray(node.subrule) ? node.subrule[0] : node.subrule;
2619
+ if (typeof target === "string") callSites.set(target, (callSites.get(target) || 0) + 1);
2620
+ }
2621
+ for (const key of Object.keys(node)) count(node[key]);
2622
+ })(rules);
2623
+ const inlining = /* @__PURE__ */ new Set();
2624
+ let coldDepth = 0;
2625
+ const cold = (write) => {
2626
+ coldDepth++;
2627
+ try {
2628
+ return write();
2629
+ } finally {
2630
+ coldDepth--;
2631
+ }
2632
+ };
2633
+ const canInline = (name) => inlineRules && coldDepth === 0 && !memo && !memoSlots.has(name) && !inlining.has(name) && inlining.size < MAX_INLINE_DEPTH && callSites.get(name) === 1 && INLINE_OPS.has(opOf(rules[name]));
1773
2634
  function indexOfRule(name) {
1774
2635
  let index = ruleIndex.get(name);
1775
2636
  if (index !== void 0) return index;
@@ -1796,7 +2657,7 @@ function createCodegen(config) {
1796
2657
  out.push(
1797
2658
  `let ${result};`,
1798
2659
  `const ${token} = c.tokens[c.idx];`,
1799
- `if (${token} !== undefined && ${token}.type === ${JSON.stringify(name)}) { c.idx++; ${result} = ${token}; }`,
2660
+ `if (${token} !== undefined && ${typeCheck(token, name)}) { c.idx++; ${result} = ${token}; }`,
1800
2661
  `else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
1801
2662
  );
1802
2663
  return result;
@@ -1830,6 +2691,16 @@ function createCodegen(config) {
1830
2691
  );
1831
2692
  return result;
1832
2693
  }
2694
+ case "flag": {
2695
+ const start = nextId();
2696
+ out.push(`let ${result};`, `const ${start} = c.idx;`, "{");
2697
+ const value = gen(arg, out, bindingsVar);
2698
+ out.push(
2699
+ `if (${value} === F) { c.idx = ${start}; ${result} = false; } else ${result} = true;`,
2700
+ "}"
2701
+ );
2702
+ return result;
2703
+ }
1833
2704
  case "many":
1834
2705
  case "many1": {
1835
2706
  const atLeastOne = op === "many1";
@@ -1847,7 +2718,7 @@ function createCodegen(config) {
1847
2718
  );
1848
2719
  }
1849
2720
  out.push("for (;;) {", `const ${before} = c.idx;`);
1850
- const value = gen(arg, out, bindingsVar);
2721
+ const value = atLeastOne ? cold(() => gen(arg, out, bindingsVar)) : gen(arg, out, bindingsVar);
1851
2722
  out.push(
1852
2723
  `if (${value} === F) { c.idx = ${before}; break; }`,
1853
2724
  `if (c.idx === ${before}) P(${JSON.stringify(op)}, ${before});`,
@@ -1877,7 +2748,7 @@ function createCodegen(config) {
1877
2748
  operatorVar = gen(arg.operator, out, bindingsVar);
1878
2749
  out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`);
1879
2750
  }
1880
- const right = gen(arg.operand, out, bindingsVar);
2751
+ const right = cold(() => gen(arg.operand, out, bindingsVar));
1881
2752
  out.push(
1882
2753
  `if (${right} === F) { c.idx = ${mark}; break; }`,
1883
2754
  `if (${bindings} === null) ${bindings} = { left: null, op: null, right: null };`,
@@ -1892,12 +2763,27 @@ function createCodegen(config) {
1892
2763
  case "oneOf": {
1893
2764
  const names = Array.isArray(arg) ? arg : [arg];
1894
2765
  const token = nextId();
1895
- const test = names.map((n) => `${token}.type === ${JSON.stringify(n)}`).join(" || ");
2766
+ const ids = tokenIds ? names.map((n) => tokenIds.get(n)) : [];
2767
+ const failed = `else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(" ")} ${result} = F; }`;
2768
+ if (names.length > ONE_OF_SWITCH_AT && ids.every((id) => id !== void 0)) {
2769
+ const hit = nextId();
2770
+ const cases = [...new Set(ids)].map((id) => `case ${id}:`).join(" ");
2771
+ out.push(
2772
+ `let ${result};`,
2773
+ `const ${token} = c.tokens[c.idx];`,
2774
+ `let ${hit} = false;`,
2775
+ `if (${token} !== undefined) { switch (${token}.id !== undefined ? ${token}.id : I[${token}.type]) { ${cases} ${hit} = true; } }`,
2776
+ `if (${hit}) { c.idx++; ${result} = ${token}; }`,
2777
+ failed
2778
+ );
2779
+ return result;
2780
+ }
2781
+ const test = names.map((n) => typeCheck(token, n)).join(" || ");
1896
2782
  out.push(
1897
2783
  `let ${result};`,
1898
2784
  `const ${token} = c.tokens[c.idx];`,
1899
2785
  `if (${token} !== undefined && (${test})) { c.idx++; ${result} = ${token}; }`,
1900
- `else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(" ")} ${result} = F; }`
2786
+ failed
1901
2787
  );
1902
2788
  return result;
1903
2789
  }
@@ -1930,7 +2816,7 @@ function createCodegen(config) {
1930
2816
  operatorVar = gen(arg.operator, out, bindingsVar);
1931
2817
  out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`);
1932
2818
  }
1933
- const right = gen(arg.operand, out, bindingsVar);
2819
+ const right = cold(() => gen(arg.operand, out, bindingsVar));
1934
2820
  out.push(
1935
2821
  `if (${right} === F) { c.idx = ${mark}; break; }`,
1936
2822
  `if (${items} === null) ${items} = [];`,
@@ -2005,6 +2891,19 @@ function createCodegen(config) {
2005
2891
  );
2006
2892
  return result;
2007
2893
  }
2894
+ case "pick":
2895
+ case "yields": {
2896
+ const label2 = "L" + nextId();
2897
+ const start = nextId();
2898
+ out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
2899
+ const values = arg.parsers.map((parser) => {
2900
+ const value = gen(parser, out, bindingsVar);
2901
+ out.push(`if (${value} === F) { c.idx = ${start}; ${result} = F; break ${label2}; }`);
2902
+ return value;
2903
+ });
2904
+ out.push(`${result} = ${op === "pick" ? values[arg.index] : JSON.stringify(arg.value)};`, "}");
2905
+ return result;
2906
+ }
2008
2907
  case "prefix": {
2009
2908
  const label2 = "L" + nextId();
2010
2909
  const start = nextId();
@@ -2081,7 +2980,7 @@ function createCodegen(config) {
2081
2980
  );
2082
2981
  const sep = gen(sepNode, out, bindingsVar);
2083
2982
  out.push(`if (${sep} === F) { c.idx = ${beforeSep}; break; }`, `const ${beforeItem} = c.idx;`);
2084
- const item = gen(arg.rule, out, bindingsVar);
2983
+ const item = cold(() => gen(arg.rule, out, bindingsVar));
2085
2984
  out.push(
2086
2985
  `if (${item} === F) { c.idx = ${trailing ? beforeItem : beforeSep}; break; }`,
2087
2986
  `${result}.push(${item});`,
@@ -2096,8 +2995,20 @@ function createCodegen(config) {
2096
2995
  out.push(`const ${result} = ${helper(planner.plan(node), { kind: "unknownRule", name })}(c);`);
2097
2996
  return result;
2098
2997
  }
2099
- const index = indexOfRule(name);
2100
2998
  const parent = nextId();
2999
+ if (canInline(name)) {
3000
+ out.push(tickSrc);
3001
+ out.push(
3002
+ `const ${parent} = c.ruleDepth;`,
3003
+ `if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`
3004
+ );
3005
+ inlining.add(name);
3006
+ const inner = gen(rules[name], out, null);
3007
+ inlining.delete(name);
3008
+ out.push(`if (c.track) c.ruleDepth = ${parent};`);
3009
+ return inner;
3010
+ }
3011
+ const index = indexOfRule(name);
2101
3012
  out.push(tickSrc);
2102
3013
  if (memo) {
2103
3014
  const key = nextId();
@@ -2118,6 +3029,26 @@ function createCodegen(config) {
2118
3029
  `c.memo.set(${key}, { value: ${result}, end: c.idx });`,
2119
3030
  "}"
2120
3031
  );
3032
+ } else if (memoSlots.has(name)) {
3033
+ const table = nextId();
3034
+ const at = nextId();
3035
+ const end = nextId();
3036
+ out.push(
3037
+ `let ${result};`,
3038
+ `const ${table} = c.rm[${memoSlots.get(name)}];`,
3039
+ `const ${at} = c.idx;`,
3040
+ `const ${end} = ${table}.ends[${at}];`,
3041
+ `if (${end} !== 0) { c.idx = ${end} - 1; ${result} = ${table}.vals[${at}]; }`,
3042
+ "else {",
3043
+ `const ${parent} = c.ruleDepth;`,
3044
+ `if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
3045
+ `${result} = R[${index}](c);`,
3046
+ `if (c.track) c.ruleDepth = ${parent};`,
3047
+ `if (${result} === F) c.idx = ${at};`,
3048
+ `${table}.ends[${at}] = c.idx + 1;`,
3049
+ `${table}.vals[${at}] = ${result};`,
3050
+ "}"
3051
+ );
2121
3052
  } else {
2122
3053
  out.push(
2123
3054
  `const ${parent} = c.ruleDepth;`,
@@ -2135,7 +3066,7 @@ function createCodegen(config) {
2135
3066
  }
2136
3067
  function genSeq(children, out, result) {
2137
3068
  if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers");
2138
- const idiom = recognizeSeqIdiom(children);
3069
+ const idiom = idioms ? recognizeSeqIdiom(children) : null;
2139
3070
  if (idiom !== null) return gen(idiom, out, null);
2140
3071
  let hasLabels = false;
2141
3072
  let hasAction = false;
@@ -2171,24 +3102,53 @@ function createCodegen(config) {
2171
3102
  out.push(`${result} = ${hasAction ? actionResult : "[" + collected.join(", ") + "]"};`, "}");
2172
3103
  return result;
2173
3104
  }
3105
+ function genOrderedBacktrack(list, out, bindingsVar) {
3106
+ const result = nextId();
3107
+ const label2 = "L" + nextId();
3108
+ const start = nextId();
3109
+ out.push(`let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
3110
+ for (const branch of list) {
3111
+ out.push("{");
3112
+ const value = gen(branch, out, bindingsVar);
3113
+ out.push(`if (${value} !== F) { ${result} = ${value}; break ${label2}; }`, `c.idx = ${start};`, "}");
3114
+ }
3115
+ out.push(`${result} = F;`, "}");
3116
+ return result;
3117
+ }
2174
3118
  function genAlt(branches, out, result, bindingsVar) {
2175
3119
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches");
3120
+ const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null;
3121
+ if (oneOf !== null) return gen(oneOf, out, bindingsVar);
3122
+ const factored = idioms ? recognizeAltPrefixIdiom(branches) : null;
3123
+ if (factored !== null) return gen(factored, out, bindingsVar);
2176
3124
  const table = firsts ? buildDispatch(branches, firsts) : null;
2177
3125
  const label2 = "L" + nextId();
2178
3126
  if (table !== null) {
2179
3127
  const expected = [...table.keys()];
3128
+ const expectedIds = tokenIds ? expected.map((t) => tokenIds.get(t)) : null;
3129
+ const numeric = expectedIds !== null && expectedIds.every((id) => id !== void 0);
2180
3130
  const token = nextId();
2181
3131
  out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${token} = c.tokens[c.idx];`);
2182
3132
  out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(" ")} ${result} = F; break ${label2}; }`);
2183
- out.push(`switch (${token}.type) {`);
3133
+ let discriminant;
3134
+ if (numeric) {
3135
+ discriminant = nextId();
3136
+ out.push(`const ${discriminant} = ${token}.id !== undefined ? ${token}.id : I[${token}.type];`);
3137
+ out.push(`switch (${discriminant}) {`);
3138
+ } else {
3139
+ discriminant = `${token}.type`;
3140
+ out.push(`switch (${discriminant}) {`);
3141
+ }
2184
3142
  const byBranch = /* @__PURE__ */ new Map();
2185
- for (const [tokenName, branch] of table) {
3143
+ for (const tokenName of expected) {
3144
+ const branch = table.get(tokenName);
2186
3145
  if (!byBranch.has(branch)) byBranch.set(branch, []);
2187
3146
  byBranch.get(branch).push(tokenName);
2188
3147
  }
3148
+ const caseLabel = (t) => numeric ? tokenIds.get(t) : JSON.stringify(t);
2189
3149
  for (const [branch, tokenNames] of byBranch) {
2190
- out.push(tokenNames.map((t) => `case ${JSON.stringify(t)}:`).join(" ") + " {");
2191
- const value = gen(branch, out, bindingsVar);
3150
+ out.push(tokenNames.map((t) => `case ${caseLabel(t)}:`).join(" ") + " {");
3151
+ const value = Array.isArray(branch) ? genOrderedBacktrack(branch, out, bindingsVar) : gen(branch, out, bindingsVar);
2192
3152
  out.push(`${result} = ${value}; break ${label2};`, "}");
2193
3153
  }
2194
3154
  out.push(
@@ -2198,16 +3158,10 @@ function createCodegen(config) {
2198
3158
  );
2199
3159
  return result;
2200
3160
  }
2201
- const start = nextId();
2202
- out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
2203
- for (const branch of branches) {
2204
- out.push("{");
2205
- const value = gen(branch, out, bindingsVar);
2206
- out.push(`if (${value} !== F) { ${result} = ${value}; break ${label2}; }`, `c.idx = ${start};`, "}");
2207
- }
2208
- out.push(`${result} = F;`, "}");
2209
- return result;
3161
+ out.push(tickSrc);
3162
+ return genOrderedBacktrack(branches, out, bindingsVar);
2210
3163
  }
3164
+ const idByName = tokenIds ? Object.fromEntries(tokenIds) : null;
2211
3165
  function buildRule(name) {
2212
3166
  const out = [];
2213
3167
  const value = gen(rules[name], out, null);
@@ -2217,8 +3171,8 @@ return ${value};
2217
3171
  }`;
2218
3172
  SOURCES[ruleIndex.get(name)] = body;
2219
3173
  const source = `return ${body}`;
2220
- const factory = new Function("F", "E", "P", "T", "R", "H", source);
2221
- return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS);
3174
+ const factory = new Function("F", "E", "P", "T", "R", "H", "I", source);
3175
+ return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS, idByName);
2222
3176
  }
2223
3177
  function progressError(op, at) {
2224
3178
  throw new Error(
@@ -2259,6 +3213,165 @@ return ${value};
2259
3213
  return { compile, functionFor, sourceFor, ruleIndex, RULES, SOURCES, ORIGINS, guard };
2260
3214
  }
2261
3215
 
3216
+ // src/memoplan.js
3217
+ function selectMemoRules(grammar, firsts, options = {}) {
3218
+ const ll1 = options.ll1 !== false;
3219
+ const names = Object.keys(grammar);
3220
+ const leftEdges = /* @__PURE__ */ new Map();
3221
+ const callEdges = /* @__PURE__ */ new Map();
3222
+ for (const name of names) {
3223
+ const left = /* @__PURE__ */ new Set();
3224
+ leftmostCalls(grammar[name], firsts, left);
3225
+ leftEdges.set(name, [...left].filter((r) => r in grammar));
3226
+ const calls = /* @__PURE__ */ new Set();
3227
+ walk(grammar[name], (node) => {
3228
+ if (opOf(node) !== "subrule") return;
3229
+ const arg = node.subrule;
3230
+ const target = Array.isArray(arg) ? arg[0] : arg;
3231
+ if (typeof target === "string" && target in grammar) calls.add(target);
3232
+ });
3233
+ callEdges.set(name, [...calls]);
3234
+ }
3235
+ const closure = (edges, from) => {
3236
+ const seen = /* @__PURE__ */ new Set();
3237
+ const stack = [...from];
3238
+ while (stack.length > 0) {
3239
+ const next = stack.pop();
3240
+ if (seen.has(next)) continue;
3241
+ seen.add(next);
3242
+ for (const edge of edges.get(next) || []) stack.push(edge);
3243
+ }
3244
+ return seen;
3245
+ };
3246
+ const leftCache = /* @__PURE__ */ new Map();
3247
+ const leftReach = (name) => {
3248
+ let reach = leftCache.get(name);
3249
+ if (reach === void 0) {
3250
+ reach = closure(leftEdges, leftEdges.get(name) || []);
3251
+ leftCache.set(name, reach);
3252
+ }
3253
+ return reach;
3254
+ };
3255
+ const startsWith = (branch) => {
3256
+ const direct = /* @__PURE__ */ new Set();
3257
+ leftmostCalls(branch, firsts, direct);
3258
+ const all = /* @__PURE__ */ new Set();
3259
+ for (const rule of direct) {
3260
+ if (!(rule in grammar)) continue;
3261
+ all.add(rule);
3262
+ for (const deeper of leftReach(rule)) all.add(deeper);
3263
+ }
3264
+ return all;
3265
+ };
3266
+ const sites = [];
3267
+ for (const name of names) {
3268
+ walk(grammar[name], (node) => {
3269
+ if (opOf(node) !== "alt" || !Array.isArray(node.alt) || node.alt.length < 2) return;
3270
+ for (const group of collisionGroups(node.alt, firsts, ll1)) {
3271
+ const counts = /* @__PURE__ */ new Map();
3272
+ for (const branch of group) {
3273
+ for (const rule of startsWith(branch)) counts.set(rule, (counts.get(rule) || 0) + 1);
3274
+ }
3275
+ const shared = [...counts].filter(([, n]) => n >= 2).map(([rule]) => rule);
3276
+ const topmost = shared.filter((rule) => !shared.some((other) => other !== rule && leftReach(other).has(rule) && !leftReach(rule).has(other)));
3277
+ if (topmost.length > 0) sites.push({ host: name, rules: topmost });
3278
+ }
3279
+ });
3280
+ }
3281
+ const hosts = new Set(sites.map((site) => site.host));
3282
+ const chosen = /* @__PURE__ */ new Set();
3283
+ for (const { rules } of sites) {
3284
+ for (const rule of rules) {
3285
+ const inside = closure(callEdges, [rule]);
3286
+ inside.add(rule);
3287
+ for (const host of hosts) {
3288
+ if (inside.has(host)) {
3289
+ chosen.add(rule);
3290
+ break;
3291
+ }
3292
+ }
3293
+ }
3294
+ }
3295
+ const dependsOnCaller = (name) => {
3296
+ const reachable = closure(callEdges, [name]);
3297
+ reachable.add(name);
3298
+ for (const rule of reachable) if (usesOuterScope(grammar[rule])) return true;
3299
+ return false;
3300
+ };
3301
+ return names.filter((name) => chosen.has(name) && !dependsOnCaller(name));
3302
+ }
3303
+ function collisionGroups(branches, firsts, ll1) {
3304
+ const table = ll1 ? buildDispatch(branches, firsts) : null;
3305
+ if (table === null) return [branches];
3306
+ const groups = /* @__PURE__ */ new Set();
3307
+ for (const entry of table.values()) if (Array.isArray(entry) && entry.length > 1) groups.add(entry);
3308
+ return [...groups];
3309
+ }
3310
+ function walk(node, visit) {
3311
+ if (Array.isArray(node)) {
3312
+ for (const child of node) walk(child, visit);
3313
+ return;
3314
+ }
3315
+ const op = opOf(node);
3316
+ if (op === null) return;
3317
+ visit(node);
3318
+ const arg = node[op];
3319
+ switch (op) {
3320
+ case "seq":
3321
+ case "alt":
3322
+ if (Array.isArray(arg)) for (const child of arg) walk(child, visit);
3323
+ break;
3324
+ case "label":
3325
+ if (Array.isArray(arg)) walk(arg[1], visit);
3326
+ break;
3327
+ case "many":
3328
+ case "many1":
3329
+ case "option":
3330
+ case "flag":
3331
+ case "text":
3332
+ case "lookahead":
3333
+ case "negLookahead":
3334
+ walk(arg, visit);
3335
+ break;
3336
+ case "as":
3337
+ walk(Array.isArray(arg) ? arg[0] : arg && arg.parser, visit);
3338
+ break;
3339
+ case "between":
3340
+ if (arg) for (const part of [arg.open, arg.body, arg.close]) walk(part, visit);
3341
+ break;
3342
+ case "pick":
3343
+ case "yields":
3344
+ if (arg && Array.isArray(arg.parsers)) for (const part of arg.parsers) walk(part, visit);
3345
+ break;
3346
+ case "prefix":
3347
+ if (arg) {
3348
+ walk(arg.operator, visit);
3349
+ walk(arg.operand, visit);
3350
+ }
3351
+ break;
3352
+ case "postfix":
3353
+ if (arg) {
3354
+ walk(arg.operand, visit);
3355
+ walk(arg.suffix, visit);
3356
+ }
3357
+ break;
3358
+ case "infixLeft":
3359
+ case "infixRight":
3360
+ if (arg) {
3361
+ walk(arg.operand, visit);
3362
+ if (arg.operator !== void 0) walk(arg.operator, visit);
3363
+ }
3364
+ break;
3365
+ case "manySep":
3366
+ case "many1Sep":
3367
+ if (arg) {
3368
+ walk(arg.rule, visit);
3369
+ if (typeof arg.sep !== "string") walk(arg.sep, visit);
3370
+ }
3371
+ break;
3372
+ }
3373
+ }
3374
+
2262
3375
  // src/parser.js
2263
3376
  var EXECUTION_MODES = /* @__PURE__ */ new Set(["generated", "interpreted"]);
2264
3377
  var GrammarError = class extends Error {
@@ -2274,6 +3387,9 @@ function createParser(spec, options = {}) {
2274
3387
  const { tokens: tokenDefs, rules, start } = spec;
2275
3388
  const {
2276
3389
  memo = false,
3390
+ autoMemo = true,
3391
+ inlineRules = true,
3392
+ idioms = true,
2277
3393
  strict = true,
2278
3394
  validate = true,
2279
3395
  maxSteps = 0,
@@ -2300,9 +3416,12 @@ function createParser(spec, options = {}) {
2300
3416
  for (const [name, method] of Object.entries(extraMethods || {})) engine.addMethod(name, method);
2301
3417
  const startRule = rules[start];
2302
3418
  const firsts = ll1 ? analysis.firsts : null;
2303
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval });
3419
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]));
3420
+ const memoRules = autoMemo && !memo && analysis.errors.length === 0 ? selectMemoRules(rules, analysis.firsts, { ll1 }) : [];
3421
+ const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]));
3422
+ const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds, memoSlots, idioms });
2304
3423
  engine.__jlGrammar = { planner };
2305
- const codegen = compile && unsafeEval ? createCodegen({ planner, rules, firsts, memo, maxSteps }) : null;
3424
+ const codegen = compile && unsafeEval ? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms }) : null;
2306
3425
  const compiledStart = codegen && codegen.compile(start);
2307
3426
  if (codegen && !engine.disableInterpretedOptimization) {
2308
3427
  for (const name of Object.keys(rules)) {
@@ -2311,7 +3430,7 @@ function createParser(spec, options = {}) {
2311
3430
  }
2312
3431
  }
2313
3432
  function parseTokens(tokens, source, track = false) {
2314
- const state = createState(tokens, rules, { memo, maxSteps, track });
3433
+ const state = createState(tokens, rules, { memo, maxSteps, track, memoSlots: memoRules.length });
2315
3434
  state.firsts = firsts;
2316
3435
  state.source = source;
2317
3436
  state.positions = lexer.positions;
@@ -2341,6 +3460,8 @@ function createParser(spec, options = {}) {
2341
3460
  planner,
2342
3461
  codegen,
2343
3462
  execution: mode,
3463
+ /** Rules memoized by `autoMemo`, for introspection. */
3464
+ memoRules,
2344
3465
  compiled: Boolean(compiledStart),
2345
3466
  /** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
2346
3467
  get firsts() {
@@ -2646,6 +3767,7 @@ var CONSTRUCTORS = {
2646
3767
  many: { arity: [1, 1], of: ([a]) => ({ many: a }) },
2647
3768
  many1: { arity: [1, 1], of: ([a]) => ({ many1: a }) },
2648
3769
  option: { arity: [1, 1], of: ([a]) => ({ option: a }) },
3770
+ flag: { arity: [1, 1], of: ([a]) => ({ flag: a }) },
2649
3771
  action: { arity: [1, 1], of: ([a]) => ({ action: a }) },
2650
3772
  lookahead: { arity: [1, 1], of: ([a]) => ({ lookahead: a }) },
2651
3773
  negLookahead: { arity: [1, 1], of: ([a]) => ({ negLookahead: a }) },
@@ -2657,6 +3779,10 @@ var CONSTRUCTORS = {
2657
3779
  manySep: { arity: [2, 2], of: ([a, b]) => ({ manySep: { rule: a, sep: b } }) },
2658
3780
  many1Sep: { arity: [2, 2], of: ([a, b]) => ({ many1Sep: { rule: a, sep: b } }) },
2659
3781
  between: { arity: [3, 3], of: ([a, b, c]) => ({ between: { open: a, body: b, close: c } }) },
3782
+ // pick(1, consume(Open), subrule(Body), consume(Close)) — the result at that index.
3783
+ pick: { arity: [2, Infinity], of: ([index, ...parsers]) => ({ pick: { parsers, index } }) },
3784
+ // yields(consume(True), true) — run the parsers, give back the constant.
3785
+ yields: { arity: [2, Infinity], of: (args) => ({ yields: { parsers: args.slice(0, -1), value: args[args.length - 1] } }) },
2660
3786
  infixLeft: { arity: [3, 3], of: ([a, b, c]) => ({ infixLeft: { operand: a, operator: b, combine: logicOf(c) } }) },
2661
3787
  infixRight: { arity: [3, 3], of: ([a, b, c]) => ({ infixRight: { operand: a, operator: b, combine: logicOf(c) } }) },
2662
3788
  prefix: { arity: [3, 3], of: ([a, b, c]) => ({ prefix: { operator: a, operand: b, combine: logicOf(c) } }) },
@@ -2677,6 +3803,355 @@ function createParserFromSource(source, options = {}) {
2677
3803
  return createParser(grammar, { ...options, methods });
2678
3804
  }
2679
3805
 
3806
+ // src/lexgen.js
3807
+ function generateLexer(defs, options = {}) {
3808
+ const positions = options.positions === "offset" ? "offset" : "full";
3809
+ const trackLines = positions === "full";
3810
+ const useScanners = options.scanners !== false;
3811
+ const useFuse = options.fuse !== false;
3812
+ const modeIds = /* @__PURE__ */ new Map([[DEFAULT_MODE2, 0]]);
3813
+ const modeNames = [DEFAULT_MODE2];
3814
+ const idOf = (name) => {
3815
+ let id = modeIds.get(name);
3816
+ if (id === void 0) {
3817
+ id = modeNames.length;
3818
+ modeIds.set(name, id);
3819
+ modeNames.push(name);
3820
+ }
3821
+ return id;
3822
+ };
3823
+ for (const def of defs) for (const mode of modesOf2(def)) idOf(mode);
3824
+ const compiled = defs.map((def, id) => {
3825
+ const flags = def.ignoreCase ? "iy" : "y";
3826
+ return {
3827
+ id,
3828
+ name: def.name,
3829
+ skip: !!def.skip,
3830
+ literal: def.literal,
3831
+ pattern: def.pattern,
3832
+ flags,
3833
+ longerAlt: def.longerAlt || null,
3834
+ scanner: useScanners && def.pattern ? buildScanner(def.pattern, !!def.ignoreCase) : null,
3835
+ modes: modesOf2(def),
3836
+ pushMode: def.pushMode ? idOf(def.pushMode) : -1,
3837
+ popMode: !!def.popMode,
3838
+ firstChars: def.literal !== void 0 ? def.literal.length > 0 ? /* @__PURE__ */ new Set([def.literal.charCodeAt(0)]) : null : firstCharSet(new RegExp(def.pattern, flags).source, !!def.ignoreCase)
3839
+ };
3840
+ });
3841
+ const byName = new Map(compiled.map((c) => [c.name, c]));
3842
+ const multiMode = modeNames.length > 1 || compiled.some((c) => c.popMode || c.pushMode !== -1);
3843
+ const regexes = [];
3844
+ const regexIndex = /* @__PURE__ */ new Map();
3845
+ const rxFor = (c) => {
3846
+ if (!regexIndex.has(c.id)) {
3847
+ regexIndex.set(c.id, regexes.length);
3848
+ regexes.push([c.id, c.flags]);
3849
+ }
3850
+ return (
3851
+ /** @type {number} */
3852
+ regexIndex.get(c.id)
3853
+ );
3854
+ };
3855
+ const fusedRuns = [];
3856
+ const helpers = [];
3857
+ const keywords = [];
3858
+ const lookFns = /* @__PURE__ */ new Map();
3859
+ const need = { digits: false, hex: false, keyword: false };
3860
+ function emitRegex(c) {
3861
+ const rx = rxFor(c);
3862
+ if (c.skip) {
3863
+ return `RX[${rx}].lastIndex = offset; if (RX[${rx}].test(text) && RX[${rx}].lastIndex > offset) { id = ${c.id}; end = RX[${rx}].lastIndex; matched = null; break found }`;
3864
+ }
3865
+ return `RX[${rx}].lastIndex = offset; m = RX[${rx}].exec(text); if (m !== null && m[0].length > 0) { id = ${c.id}; matched = m[0]; end = offset + matched.length; break found }`;
3866
+ }
3867
+ const emitted = /* @__PURE__ */ new Map();
3868
+ function emitDef(c) {
3869
+ let code = emitted.get(c.id);
3870
+ if (code === void 0) {
3871
+ code = emitDefOnce(c);
3872
+ emitted.set(c.id, code);
3873
+ }
3874
+ return code;
3875
+ }
3876
+ function emitDefOnce(c) {
3877
+ if (c.literal !== void 0) {
3878
+ const lit = c.literal;
3879
+ const test = lit.length === 1 ? `code === ${lit.charCodeAt(0)}` : `text.startsWith(${q(lit)}, offset)`;
3880
+ return `if (${test}) { id = ${c.id}; end = offset + ${lit.length}; matched = ${q(lit)}; break found }`;
3881
+ }
3882
+ const s = c.scanner;
3883
+ if (s === null) return emitRegex(c);
3884
+ const hit = `{ id = ${c.id}; end = e; matched = null; break found }`;
3885
+ switch (s.kind) {
3886
+ case SCANNER_KINDS.CLASS: {
3887
+ if (s.wide) {
3888
+ helpers.push(wideClassHelper(c.id, s));
3889
+ return `{ const e = scan${c.id}(text, offset, length); if (e === -2) { ${emitRegex(c)} } else if (e !== -1) ${hit} }`;
3890
+ }
3891
+ return `if (${condition(s.first, "code")}) { let j = offset + 1; while (j < length) { const c = text.charCodeAt(j); if (!(${condition(s.rest, "c")})) break; j++ } id = ${c.id}; end = j; matched = null; break found }`;
3892
+ }
3893
+ case SCANNER_KINDS.SEQ2:
3894
+ return `if (${condition(s.first, "code")}) { let j = offset + 1; while (j < length) { const c = text.charCodeAt(j); if (${condition(s.first, "c")}) j++; else break } const mid = j; while (j < length) { const c = text.charCodeAt(j); if (${condition(s.second, "c")}) j++; else break } if (j > mid) { id = ${c.id}; end = j; matched = null; break found } }`;
3895
+ case SCANNER_KINDS.KEYWORD: {
3896
+ need.keyword = true;
3897
+ const cond = condition(s.look, "c");
3898
+ let look = lookFns.get(cond);
3899
+ if (look === void 0) {
3900
+ look = `look${lookFns.size}`;
3901
+ lookFns.set(cond, look);
3902
+ }
3903
+ keywords.push([s.lower, s.upper]);
3904
+ return `{ const e = scanKeyword(text, offset, length, KW[${keywords.length - 1}][0], KW[${keywords.length - 1}][1], ${look}); if (e !== -1) ${hit} }`;
3905
+ }
3906
+ case SCANNER_KINDS.LINE:
3907
+ return `if (text.startsWith(${q(s.prefix)}, offset)) { let j = offset + ${s.prefix.length}; while (j < length) { const c = text.charCodeAt(j); if (${condition(s.look, "c")}) break; j++ } id = ${c.id}; end = j; matched = null; break found }`;
3908
+ case SCANNER_KINDS.QUOTED:
3909
+ helpers.push(quotedHelper(c.id, s));
3910
+ return `{ const e = scan${c.id}(text, offset); if (e !== -1) ${hit} }`;
3911
+ case SCANNER_KINDS.NUMBER:
3912
+ need.digits = true;
3913
+ helpers.push(numberHelper(c.id, s));
3914
+ return `{ const e = scan${c.id}(text, offset, length); if (e !== -1) ${hit} }`;
3915
+ case SCANNER_KINDS.STRING:
3916
+ if (s.uni) need.hex = true;
3917
+ helpers.push(stringHelper(c.id, s));
3918
+ return `{ const e = scan${c.id}(text, offset, length); if (e !== -1) ${hit} }`;
3919
+ default:
3920
+ return emitRegex(c);
3921
+ }
3922
+ }
3923
+ const fusible = (c) => c.literal === void 0 && c.scanner === null && !/\\[1-9]|\\k</.test(c.pattern) && new RegExp(c.pattern + "|").exec("").length === 1;
3924
+ function emitList(list) {
3925
+ const out = [];
3926
+ for (let i = 0; i < list.length; ) {
3927
+ let j = i;
3928
+ while (useFuse && j < list.length && fusible(list[j]) && list[j].flags === list[i].flags) j++;
3929
+ if (j - i < 3) {
3930
+ out.push(emitDef(list[i]));
3931
+ i++;
3932
+ continue;
3933
+ }
3934
+ const run = list.slice(i, j);
3935
+ run.forEach(rxFor);
3936
+ const k = fusedRuns.length;
3937
+ fusedRuns.push({ flags: run[0].flags, ids: run.map((d) => d.id), rx: run.map((d) => (
3938
+ /** @type {number} */
3939
+ regexIndex.get(d.id)
3940
+ )) });
3941
+ out.push(
3942
+ `FX[${k}].lastIndex = offset; m = FX[${k}].exec(text)
3943
+ if (m !== null) {
3944
+ if (m[0].length > 0) { let g = 1; while (m[g] === undefined) g++; id = FIDS[${k}][g - 1]; matched = m[0]; end = offset + matched.length; break found }
3945
+ // An empty match is "no match" to a lexer, so the members after it still get their turn.
3946
+ for (let k = 0; k < FRX[${k}].length; k++) { const r = RX[FRX[${k}][k]]; r.lastIndex = offset; const mm = r.exec(text); if (mm !== null && mm[0].length > 0) { id = FIDS[${k}][k]; matched = mm[0]; end = offset + matched.length; break found } }
3947
+ }`
3948
+ );
3949
+ i = j;
3950
+ }
3951
+ return out.join("\n ");
3952
+ }
3953
+ const lists = [{ list: (
3954
+ /** @type {any[]} */
3955
+ []
3956
+ ) }];
3957
+ const listIds = /* @__PURE__ */ new Map([["", 0]]);
3958
+ const listFor = (list) => {
3959
+ const key = list.map((c) => c.id).join(",");
3960
+ let id = listIds.get(key);
3961
+ if (id === void 0) {
3962
+ id = lists.length;
3963
+ listIds.set(key, id);
3964
+ lists.push({ list });
3965
+ }
3966
+ return id;
3967
+ };
3968
+ const WIDTH = 129;
3969
+ const table = [];
3970
+ for (const name of modeNames) {
3971
+ const members = compiled.filter((c) => c.modes.includes(name));
3972
+ if (members.length === 0) throw new Error(`generateLexer: mode '${name}' has no tokens`);
3973
+ const dispatch = members.some((c) => c.firstChars !== null);
3974
+ for (let code = 0; code < 128; code++) {
3975
+ table.push(listFor(dispatch ? members.filter((c) => c.firstChars === null || c.firstChars.has(code)) : members));
3976
+ }
3977
+ table.push(listFor(dispatch ? members.filter((c) => c.firstChars === null || [...c.firstChars].some((x) => x >= 128)) : members));
3978
+ }
3979
+ if (lists.length > 6e4) throw new Error("generateLexer: too many distinct candidate lists");
3980
+ const dispatchTable = String.fromCharCode(...table.map((id) => 256 + id));
3981
+ const listCases = lists.slice(1).map(({ list }, i) => `case ${i + 1}: {
3982
+ ${emitList(list)}
3983
+ break
3984
+ }`);
3985
+ const longer = compiled.filter((c) => c.longerAlt);
3986
+ const longerCode = longer.length === 0 ? "" : `
3987
+ switch (id) {
3988
+ ${longer.map((c) => {
3989
+ const alt2 = (
3990
+ /** @type {any} */
3991
+ byName.get(
3992
+ /** @type {string} */
3993
+ c.longerAlt
3994
+ )
3995
+ );
3996
+ const altEnd = alt2.literal !== void 0 ? `text.startsWith(${q(alt2.literal)}, offset) ? offset + ${alt2.literal.length} : -1` : `(RX[${rxFor(alt2)}].lastIndex = offset, RX[${rxFor(alt2)}].test(text) ? RX[${rxFor(alt2)}].lastIndex : -1)`;
3997
+ return ` case ${c.id}: { const altEnd = ${altEnd}; if (altEnd > end) { end = altEnd; id = ${alt2.id}; matched = ${alt2.literal !== void 0 ? q(alt2.literal) : "null"} } break }`;
3998
+ }).join("\n")}
3999
+ }`;
4000
+ const transitions = compiled.filter((c) => c.popMode || c.pushMode !== -1);
4001
+ const describe = trackLines ? "'line ' + line + ' column ' + (offset - lineStart + 1)" : "'offset ' + offset";
4002
+ const transitionCode = transitions.length === 0 ? "" : `
4003
+ switch (id) {
4004
+ ${transitions.map((c) => c.popMode ? ` case ${c.id}:
4005
+ if (depth === 0) throw new LexError('Token \\'' + NAMES[id] + '\\' popped the initial lexer mode at ' + ${describe}, { offset, line: ${trackLines ? "line" : "0"}, col: ${trackLines ? "offset - lineStart + 1" : "0"} })
4006
+ mode = stack[--depth]
4007
+ break` : ` case ${c.id}: stack[depth++] = mode; mode = ${c.pushMode}; break`).join("\n")}
4008
+ }`;
4009
+ const where = trackLines ? "'line ' + line + ' column ' + column" : "'offset ' + offset";
4010
+ const inMode = modeNames.length > 1 ? " + ' (in lexer mode \\'' + MODE_NAMES[mode] + '\\')'" : "";
4011
+ return `(() => {
4012
+ const NAMES = tokens.map((t) => t.name)
4013
+ const SKIP = tokens.map((t) => (t.skip ? 1 : 0))
4014
+ const DISPATCH = ${q(dispatchTable)}
4015
+ ${modeNames.length > 1 ? `const MODE_NAMES = ${q(modeNames)}` : ""}
4016
+ // Patterns are read from the token table above rather than repeated here.
4017
+ const RX = ${q(regexes)}.map(([i, flags]) => new RegExp(tokens[i].pattern, flags))
4018
+ ${fusedRuns.length ? `const FX = ${q(fusedRuns.map((f) => [f.ids, f.flags]))}.map(([ids, flags]) => new RegExp(ids.map((i) => '(' + tokens[i].pattern + ')').join('|'), flags))
4019
+ const FIDS = ${q(fusedRuns.map((f) => f.ids))}
4020
+ const FRX = ${q(fusedRuns.map((f) => f.rx))}` : ""}
4021
+ ${need.digits ? "const skipDigits = (text, i, length) => { while (i < length) { const c = text.charCodeAt(i); if (c < 48 || c > 57) break; i++ } return i }" : ""}
4022
+ ${need.hex ? "const isHex = (c) => (c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102)" : ""}
4023
+ ${need.keyword ? `const KW = ${q(keywords)}
4024
+ ${[...lookFns].map(([cond, name]) => `const ${name} = (c) => ${cond}`).join("\n ")}
4025
+ ${KEYWORD_HELPER}` : ""}
4026
+ ${helpers.join("\n")}
4027
+
4028
+ function tokenize (text) {
4029
+ if (typeof text !== 'string') throw new TypeError('tokenize: expected a string')
4030
+ const tokens_ = []
4031
+ let offset = 0
4032
+ let line = 1
4033
+ let lineStart = 0
4034
+ const length = text.length
4035
+ ${multiMode ? "let mode = 0\n const stack = []\n let depth = 0" : ""}
4036
+ let m = null
4037
+ while (offset < length) {
4038
+ const code = text.charCodeAt(offset)
4039
+ let id = -1
4040
+ let end = -1
4041
+ let matched = null
4042
+ found: {
4043
+ switch (DISPATCH.charCodeAt(${multiMode ? `mode * ${WIDTH} + ` : ""}(code < 128 ? code : 128)) - 256) {
4044
+ ${listCases.join("\n ")}
4045
+ }
4046
+ }
4047
+ if (id === -1) {
4048
+ const column = offset - lineStart + 1
4049
+ throw new LexError('Unexpected character \\'' + text[offset] + '\\' at ' + ${where}${inMode}, { offset, line: ${trackLines ? "line" : "0"}, col: ${trackLines ? "column" : "0"} })
4050
+ }${longerCode}
4051
+ if (SKIP[id] === 0) {
4052
+ const image = matched !== null ? matched : text.slice(offset, end)
4053
+ tokens_.push(${trackLines ? "{ type: NAMES[id], image, start: offset, end, line, col: offset - lineStart + 1 }" : "{ type: NAMES[id], id, image, start: offset, end }"})
4054
+ }
4055
+ ${trackLines ? "for (let j = offset; j < end; j++) { if (text.charCodeAt(j) !== 10) continue; line++; lineStart = j + 1 }" : ""}${transitionCode}
4056
+ offset = end
4057
+ }
4058
+ return tokens_
4059
+ }
4060
+ return { tokenize, tokenNames: NAMES, positions: ${q(positions)}, modeNames: ${q(modeNames)} }
4061
+ })()`;
4062
+ }
4063
+ var DEFAULT_MODE2 = "default";
4064
+ var q = JSON.stringify;
4065
+ function modesOf2(def) {
4066
+ if (Array.isArray(def.modes) && def.modes.length > 0) return def.modes;
4067
+ if (def.mode) return [def.mode];
4068
+ return [DEFAULT_MODE2];
4069
+ }
4070
+ function condition(table, v) {
4071
+ const parts = [];
4072
+ for (let i = 0; i < 128; ) {
4073
+ if (table[i] !== 1) {
4074
+ i++;
4075
+ continue;
4076
+ }
4077
+ let j = i;
4078
+ while (j + 1 < 128 && table[j + 1] === 1) j++;
4079
+ parts.push(i === j ? `${v} === ${i}` : `(${v} >= ${i} && ${v} <= ${j})`);
4080
+ i = j + 1;
4081
+ }
4082
+ return parts.length > 0 ? parts.join(" || ") : "false";
4083
+ }
4084
+ var KEYWORD_HELPER = `function scanKeyword (text, offset, length, lower, upper, look) {
4085
+ const n = lower.length
4086
+ if (offset + n > length) return -1
4087
+ for (let i = 0; i < n; i++) {
4088
+ const c = text.charCodeAt(offset + i)
4089
+ if (c !== lower[i] && c !== upper[i]) return -1
4090
+ }
4091
+ const end = offset + n
4092
+ if (end < length && look(text.charCodeAt(end))) return -1
4093
+ return end
4094
+ }`;
4095
+ function wideClassHelper(id, s) {
4096
+ return `function scan${id} (text, offset, length) {
4097
+ const code = text.charCodeAt(offset)
4098
+ if (code >= 128) return -2
4099
+ if (!(${condition(s.first, "code")})) return -1
4100
+ let j = offset + 1
4101
+ while (j < length) {
4102
+ const c = text.charCodeAt(j)
4103
+ if (c >= 128) return -2
4104
+ if (!(${condition(s.rest, "c")})) break
4105
+ j++
4106
+ }
4107
+ return j
4108
+ }`;
4109
+ }
4110
+ function quotedHelper(id, s) {
4111
+ return `function scan${id} (text, offset) {
4112
+ if (text.charCodeAt(offset) !== ${s.quote}) return -1
4113
+ let fallback = -1
4114
+ let j = offset + 1
4115
+ for (;;) {
4116
+ j = text.indexOf(${q(String.fromCharCode(s.quote))}, j)
4117
+ if (j === -1) return fallback
4118
+ if (text.charCodeAt(j + 1) !== ${s.quote}) return j + 1
4119
+ fallback = j + 1
4120
+ j += 2
4121
+ }
4122
+ }`;
4123
+ }
4124
+ function numberHelper(id, s) {
4125
+ return `function scan${id} (text, offset, length) {
4126
+ let j = offset
4127
+ ${s.sign ? "if (text.charCodeAt(j) === 45) j++" : ""}
4128
+ const c = text.charCodeAt(j)
4129
+ ${s.jsonInt ? "if (c === 48) j++\n else if (c >= 49 && c <= 57) j = skipDigits(text, j + 1, length)\n else return -1" : "if (!(c >= 48 && c <= 57)) return -1\n j = skipDigits(text, j + 1, length)"}
4130
+ ${s.frac ? "if (text.charCodeAt(j) === 46) { const k = skipDigits(text, j + 1, length); if (k > j + 1) j = k }" : ""}
4131
+ ${s.exp ? "const e = text.charCodeAt(j)\n if (e === 101 || e === 69) { let k = j + 1; const sg = text.charCodeAt(k); if (sg === 43 || sg === 45) k++; const end = skipDigits(text, k, length); if (end > k) j = end }" : ""}
4132
+ return j
4133
+ }`;
4134
+ }
4135
+ function stringHelper(id, s) {
4136
+ return `function scan${id} (text, offset, length) {
4137
+ if (text.charCodeAt(offset) !== ${s.quote}) return -1
4138
+ let j = offset + 1
4139
+ for (;;) {
4140
+ if (j >= length) return -1
4141
+ const c = text.charCodeAt(j)
4142
+ if (c === ${s.quote}) return j + 1
4143
+ if (c === 92) {
4144
+ const d = text.charCodeAt(j + 1)
4145
+ ${s.escAny ? "if (!(d >= 0) || d === 10 || d === 13 || d === 0x2028 || d === 0x2029) return -1\n j += 2" : `if (${condition(s.esc, "d")}) j += 2
4146
+ ${s.uni ? "else if (d === 117 && isHex(text.charCodeAt(j + 2)) && isHex(text.charCodeAt(j + 3)) && isHex(text.charCodeAt(j + 4)) && isHex(text.charCodeAt(j + 5))) j += 6\n " : ""}else return -1`}
4147
+ continue
4148
+ }
4149
+ ${s.ctrl ? "if (c < 32) return -1" : ""}
4150
+ j++
4151
+ }
4152
+ }`;
4153
+ }
4154
+
2680
4155
  // src/emit.js
2681
4156
  import { Compiler, LogicEngine as LogicEngine2 } from "json-logic-engine";
2682
4157
  var UNSUPPORTED = /\b(coerceArray|precoerceNumber|compareCheck|assertAllowedDepth|assertSize|asyncIterators|notTraversed)\b|(?<!engine\.)\bmethods\[|engine\.(?!methods\b)/;
@@ -2725,11 +4200,21 @@ function emitModule(spec, options = {}) {
2725
4200
  const { tokens: tokenDefs, rules, start } = spec;
2726
4201
  const {
2727
4202
  memo = false,
4203
+ autoMemo = true,
4204
+ // Off here, on in `createParser`. Inlining a precedence chain is worth 2-3x on a bare chain but only
4205
+ // 1.02-1.04x on the formula and SQL examples, and it makes the file bigger: the formula module grows
4206
+ // 32% minified (38 to 50 KB) even after the second copy of each operand is kept as a call. A parser
4207
+ // built at run time pays nothing for that; a file someone ships does.
4208
+ inlineRules = false,
4209
+ idioms = true,
2728
4210
  strict = true,
2729
4211
  ll1 = true,
2730
4212
  maxSteps = 0,
2731
4213
  positions = "full",
2732
4214
  execution = "generated",
4215
+ lexer: lexerMode = "generated",
4216
+ scanners = true,
4217
+ fuse = true,
2733
4218
  format = "esm",
2734
4219
  validate = true,
2735
4220
  moduleName = spec.name || "parser",
@@ -2738,6 +4223,7 @@ function emitModule(spec, options = {}) {
2738
4223
  methods: semanticMethods
2739
4224
  } = options;
2740
4225
  if (format !== "esm" && format !== "cjs") throw new Error(`emitModule: format must be 'esm' or 'cjs', got ${quote(format)}`);
4226
+ if (lexerMode !== "generated" && lexerMode !== "runtime") throw new Error(`emitModule: lexer must be 'generated' or 'runtime', got ${quote(lexerMode)}`);
2741
4227
  const methodsBlock = options.methodsBlock !== void 0 ? options.methodsBlock : spec.methodsBlock;
2742
4228
  const block = methodsBlock ? emitMethodsBlock(methodsBlock, format) : null;
2743
4229
  if (!start || !(start in rules)) throw new Error(`emitModule: start rule '${start}' is not defined in rules`);
@@ -2745,12 +4231,15 @@ function emitModule(spec, options = {}) {
2745
4231
  const analysis = analyze(rules, { tokenNames: lexer.tokenNames });
2746
4232
  if (validate && analysis.errors.length > 0) throw new GrammarError(analysis.errors);
2747
4233
  const firsts = ll1 ? analysis.firsts : null;
4234
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]));
2748
4235
  const engine = { build: () => {
2749
4236
  throw new Error("not compiled during emit");
2750
4237
  }, run: () => {
2751
4238
  } };
2752
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false });
2753
- const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps });
4239
+ const memoRules = autoMemo && !memo && analysis.errors.length === 0 ? selectMemoRules(rules, analysis.firsts, { ll1 }) : [];
4240
+ const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]));
4241
+ const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false, tokenIds, memoSlots, idioms });
4242
+ const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms });
2754
4243
  codegen.compile(start);
2755
4244
  const reached = [...codegen.ruleIndex.entries()].sort((a, b) => a[1] - b[1]);
2756
4245
  const unsupported = codegen.ORIGINS.map((origin, index) => ({ origin, index })).filter(({ origin }) => origin.kind === "unknownRule");
@@ -2791,7 +4280,8 @@ R[${index}] = ${codegen.SOURCES[index]}`
2791
4280
  ).join("\n\n");
2792
4281
  const interpretedActions = execution === "interpreted";
2793
4282
  const cjs = format === "cjs";
2794
- const runtimeImports = ["FAIL as F", "expect as E", "createState", "buildParseError", "createLexer"];
4283
+ const generatedLexer = lexerMode === "generated";
4284
+ const runtimeImports = ["FAIL as F", "expect as E", "createState", "buildParseError", generatedLexer ? "LexError" : "createLexer"];
2795
4285
  if (needsEngine) runtimeImports.push("createValueMethods");
2796
4286
  const importRuntime = cjs ? `const { ${runtimeImports.map((b) => b.replace(" as ", ": ")).join(", ")} } = require(${quote(runtimeSpecifier)})` : `import { ${runtimeImports.join(", ")} } from ${quote(runtimeSpecifier)}`;
2797
4287
  const importEngine = engineFree ? "" : cjs ? `const { LogicEngine } = require(${quote(engineSpecifier)})
@@ -2810,7 +4300,7 @@ export default { ${PUBLIC.join(", ")} }`;
2810
4300
  //
2811
4301
  // The grammar was analyzed when this file was written: FIRST sets, left-recursion
2812
4302
  // checks and rule specialization all happened then. Importing it does no grammar
2813
- // work and generates no code.
4303
+ // work and generates no code.${generatedLexer ? "\n// The lexer was written out as source too, so there is no lexer engine to load." : ""}
2814
4304
  ${engineFree ? "// Every semantic action was compiled to source too, so this file has no\n// dependency on json-logic-engine at all." : interpretedActions ? "// Semantic actions are interpreted, so nothing here calls `eval`." : "// Semantic actions that could not be compiled to source are shipped as JSON\n// Logic and built by json-logic-engine when this module loads."}
2815
4305
 
2816
4306
  ${importRuntime}
@@ -2820,7 +4310,7 @@ const tokens = ${literal(tokenDefs)}
2820
4310
 
2821
4311
  const start = ${quote(start)}
2822
4312
 
2823
- const lexer = createLexer(tokens, { positions: ${quote(positions)} })
4313
+ const lexer = ${generatedLexer ? generateLexer(tokenDefs, { positions, scanners, fuse }) : `createLexer(tokens, { positions: ${quote(positions)}, scanners: ${scanners}, fuse: ${fuse} })`}
2824
4314
  const tokenize = lexer.tokenize
2825
4315
 
2826
4316
  ${engineFree ? `// Every semantic action compiled to source, so the engine reduced to a table
@@ -2893,6 +4383,14 @@ const H = [
2893
4383
  ${helpers.map((source, index) => ` /* ${index} */ ${source}`).join(",\n")}
2894
4384
  ]
2895
4385
 
4386
+ /**
4387
+ * Name -> id, the same numbering \`lexer\` assigns. A generated \`alt\`'s switch
4388
+ * dispatches on a token's id when it has one; this is only read as a fallback,
4389
+ * for a hand-built token from \`parseTokens\` that bypassed this file's own
4390
+ * lexer and so has no id of its own.
4391
+ */
4392
+ const I = ${literal(Object.fromEntries(tokenIds))}
4393
+
2896
4394
  ${ruleSources}
2897
4395
 
2898
4396
  const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name, index])))}
@@ -2903,7 +4401,7 @@ const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name,
2903
4401
  * @param {string} [source]
2904
4402
  */
2905
4403
  function parseTokens (tokenList, source, track = false) {
2906
- const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track })
4404
+ const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track, memoSlots: ${memoRules.length} })
2907
4405
  state.source = source
2908
4406
  state.positions = ${quote(positions)}
2909
4407
  state.ruleNames[0] = start