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/cjs/index.js CHANGED
@@ -365,7 +365,338 @@ function firstCharSet(source, ignoreCase) {
365
365
  return found;
366
366
  }
367
367
 
368
- // src/lexer.js
368
+ // src/scan.js
369
+ var DEFER = -2;
370
+ var CLASS = 0;
371
+ var SEQ2 = 1;
372
+ var KEYWORD = 2;
373
+ var LINE = 3;
374
+ var QUOTED = 4;
375
+ var NUMBER = 5;
376
+ var STRING = 6;
377
+ var SCANNER_KINDS = Object.freeze({ CLASS, SEQ2, KEYWORD, LINE, QUOTED, NUMBER, STRING });
378
+ var NONE = new Uint8Array(128);
379
+ function make(kind, fields) {
380
+ return {
381
+ kind,
382
+ wide: false,
383
+ first: NONE,
384
+ rest: NONE,
385
+ second: NONE,
386
+ look: NONE,
387
+ lower: [],
388
+ upper: [],
389
+ prefix: "",
390
+ quote: -1,
391
+ sign: false,
392
+ jsonInt: false,
393
+ frac: false,
394
+ exp: false,
395
+ ctrl: false,
396
+ uni: false,
397
+ escAny: false,
398
+ esc: NONE,
399
+ ...fields
400
+ };
401
+ }
402
+ var SHORTHAND = {
403
+ d: (t) => {
404
+ for (let k = 48; k < 58; k++) t[k] = 1;
405
+ },
406
+ w: (t) => {
407
+ for (let k = 0; k < 128; k++) if (/\w/.test(String.fromCharCode(k))) t[k] = 1;
408
+ },
409
+ s: (t) => {
410
+ for (const k of [9, 10, 11, 12, 13, 32]) t[k] = 1;
411
+ }
412
+ };
413
+ var SIMPLE_ESCAPES2 = { t: " ", r: "\r", n: "\n", f: "\f", v: "\v" };
414
+ var ALNUM = /[a-zA-Z0-9]/;
415
+ function parseClass(body) {
416
+ if (body.length === 0 || body[0] === "^") return null;
417
+ const table = new Uint8Array(128);
418
+ for (let i = 0; i < body.length; i++) {
419
+ let c = body[i];
420
+ if (c === "\\") {
421
+ const n = body[++i];
422
+ if (n === "d" || n === "w") {
423
+ SHORTHAND[n](table);
424
+ continue;
425
+ }
426
+ if (n === void 0) return null;
427
+ if (ALNUM.test(n) && !(n in SIMPLE_ESCAPES2)) return null;
428
+ c = SIMPLE_ESCAPES2[n] ?? n;
429
+ }
430
+ if (body[i + 1] === "-" && i + 2 < body.length) {
431
+ let hi = body[i + 2];
432
+ let advance = 2;
433
+ if (hi === "\\") {
434
+ hi = body[i + 3];
435
+ advance = 3;
436
+ if (hi === void 0 || ALNUM.test(hi)) return null;
437
+ }
438
+ const from = c.charCodeAt(0);
439
+ const to = hi.charCodeAt(0);
440
+ if (from > to || to >= 128) return null;
441
+ for (let k = from; k <= to; k++) table[k] = 1;
442
+ i += advance;
443
+ continue;
444
+ }
445
+ const code = c.charCodeAt(0);
446
+ if (code >= 128) return null;
447
+ table[code] = 1;
448
+ }
449
+ return table;
450
+ }
451
+ var BODY = String.raw`((?:[^\]\\]|\\.)*)`;
452
+ var RE_CLASS_RUN = new RegExp(String.raw`^\[${BODY}\]([+*])$`);
453
+ var RE_CLASS_PAIR = new RegExp(String.raw`^\[${BODY}\]\[${BODY}\]\*$`);
454
+ var RE_CLASS_SEQ = new RegExp(String.raw`^\[${BODY}\]\+\[${BODY}\]\+$`);
455
+ var RE_SHORTHAND = /^\\([sdw])[+*]$/;
456
+ var RE_KEYWORD = new RegExp(String.raw`^([A-Za-z0-9_]+)\(\?!\[${BODY}\]\)$`);
457
+ var RE_LINE = new RegExp(String.raw`^([^\\\[\]()*+?.|^$]+)\[\^${BODY}\]\*$`);
458
+ var RE_QUOTED = /^(['"])\(\?:\1\1\|\[\^\1\]\)\*\1$/;
459
+ var RE_NUMBER = /^(-\?)?(?:(\(\?:0\|\[1-9\]\\d\*\))|\\d\+)(\(\?:\\\.\\d\+\)\?)?(\(\?:\[eE\]\[\+-\]\?\\d\+\)\?)?$/;
460
+ var RE_STRING_ESC = new RegExp(String.raw`^(["'])\(\?:\[\^\1\\\\(\\u0000-\\u001f|\\x00-\\x1f)?\]\|\\\\\(\?:\[${BODY}\](\|u\[0-9a-fA-F\]\{4\})?\)\)\*\1$`);
461
+ var RE_STRING_ANY = new RegExp(String.raw`^(["'])\(\?:\[\^\1\\\\\]\|\\\\\.\)\*\1$`);
462
+ function buildScanner(source, ignoreCase) {
463
+ let m = RE_KEYWORD.exec(source);
464
+ if (m) {
465
+ const look = parseClass(m[2]);
466
+ if (look === null) return null;
467
+ const word = m[1];
468
+ const lower = [];
469
+ const upper = [];
470
+ for (const ch of word) {
471
+ lower.push((ignoreCase ? ch.toLowerCase() : ch).charCodeAt(0));
472
+ upper.push((ignoreCase ? ch.toUpperCase() : ch).charCodeAt(0));
473
+ }
474
+ return make(KEYWORD, { look, lower, upper });
475
+ }
476
+ if (ignoreCase) return null;
477
+ m = RE_STRING_ESC.exec(source);
478
+ if (m) {
479
+ const esc = parseClass(m[3]);
480
+ if (esc === null) return null;
481
+ return make(STRING, { quote: m[1].charCodeAt(0), ctrl: m[2] !== void 0, uni: m[4] !== void 0, esc });
482
+ }
483
+ m = RE_STRING_ANY.exec(source);
484
+ if (m) return make(STRING, { quote: m[1].charCodeAt(0), escAny: true });
485
+ m = RE_NUMBER.exec(source.replace(/\[0-9\]/g, "\\d"));
486
+ if (m && (m[1] || m[2] || m[3] || m[4])) {
487
+ return make(NUMBER, { sign: m[1] !== void 0, jsonInt: m[2] !== void 0, frac: m[3] !== void 0, exp: m[4] !== void 0 });
488
+ }
489
+ m = RE_CLASS_RUN.exec(source);
490
+ if (m) {
491
+ const table = parseClass(m[1]);
492
+ return table && make(CLASS, { first: table, rest: table });
493
+ }
494
+ m = RE_CLASS_PAIR.exec(source);
495
+ if (m) {
496
+ const first = parseClass(m[1]);
497
+ const rest = parseClass(m[2]);
498
+ return first && rest && make(CLASS, { first, rest });
499
+ }
500
+ m = RE_CLASS_SEQ.exec(source);
501
+ if (m) {
502
+ const first = parseClass(m[1]);
503
+ const second = parseClass(m[2]);
504
+ if (first && second && first.every((v, i) => !(v && second[i]))) {
505
+ return make(SEQ2, { first, rest: first, second });
506
+ }
507
+ return null;
508
+ }
509
+ m = RE_SHORTHAND.exec(source);
510
+ if (m) {
511
+ const table = new Uint8Array(128);
512
+ SHORTHAND[m[1]](table);
513
+ return make(CLASS, { first: table, rest: table, wide: m[1] === "s" });
514
+ }
515
+ m = RE_QUOTED.exec(source);
516
+ if (m) return make(QUOTED, { quote: m[1].charCodeAt(0) });
517
+ m = RE_LINE.exec(source);
518
+ if (m) {
519
+ const stop = parseClass(m[2]);
520
+ return stop && make(LINE, { prefix: m[1], look: stop });
521
+ }
522
+ return null;
523
+ }
524
+ var skipDigits = (text, i, length) => {
525
+ while (i < length) {
526
+ const c = text.charCodeAt(i);
527
+ if (c < 48 || c > 57) break;
528
+ i++;
529
+ }
530
+ return i;
531
+ };
532
+ var isHex = (c) => c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
533
+ function scanEnd(sc, text, offset, length) {
534
+ switch (sc.kind) {
535
+ case CLASS: {
536
+ const code = text.charCodeAt(offset);
537
+ if (code >= 128) return sc.wide ? DEFER : -1;
538
+ if (sc.first[code] !== 1) return -1;
539
+ const rest = sc.rest;
540
+ let j = offset + 1;
541
+ while (j < length) {
542
+ const c = text.charCodeAt(j);
543
+ if (c >= 128) return sc.wide ? DEFER : j;
544
+ if (rest[c] !== 1) break;
545
+ j++;
546
+ }
547
+ return j;
548
+ }
549
+ case SEQ2: {
550
+ const c0 = text.charCodeAt(offset);
551
+ if (c0 >= 128 || sc.first[c0] !== 1) return -1;
552
+ const { first, second } = sc;
553
+ let j = offset + 1;
554
+ while (j < length) {
555
+ const c = text.charCodeAt(j);
556
+ if (c < 128 && first[c] === 1) j++;
557
+ else break;
558
+ }
559
+ const mid = j;
560
+ while (j < length) {
561
+ const c = text.charCodeAt(j);
562
+ if (c < 128 && second[c] === 1) j++;
563
+ else break;
564
+ }
565
+ return j > mid ? j : -1;
566
+ }
567
+ case KEYWORD: {
568
+ const { lower, upper } = sc;
569
+ const n = lower.length;
570
+ if (offset + n > length) return -1;
571
+ for (let i = 0; i < n; i++) {
572
+ const c = text.charCodeAt(offset + i);
573
+ if (c !== lower[i] && c !== upper[i]) return -1;
574
+ }
575
+ const end = offset + n;
576
+ if (end < length) {
577
+ const c = text.charCodeAt(end);
578
+ if (c < 128 && sc.look[c] === 1) return -1;
579
+ }
580
+ return end;
581
+ }
582
+ case LINE: {
583
+ if (!text.startsWith(sc.prefix, offset)) return -1;
584
+ let j = offset + sc.prefix.length;
585
+ const stop = sc.look;
586
+ while (j < length) {
587
+ const c = text.charCodeAt(j);
588
+ if (c < 128 && stop[c] === 1) break;
589
+ j++;
590
+ }
591
+ return j;
592
+ }
593
+ case NUMBER: {
594
+ let j = offset;
595
+ if (sc.sign && text.charCodeAt(j) === 45) j++;
596
+ const c = text.charCodeAt(j);
597
+ if (sc.jsonInt) {
598
+ if (c === 48) j++;
599
+ else if (c >= 49 && c <= 57) j = skipDigits(text, j + 1, length);
600
+ else return -1;
601
+ } else {
602
+ if (!(c >= 48 && c <= 57)) return -1;
603
+ j = skipDigits(text, j + 1, length);
604
+ }
605
+ if (sc.frac && text.charCodeAt(j) === 46) {
606
+ const k = skipDigits(text, j + 1, length);
607
+ if (k > j + 1) j = k;
608
+ }
609
+ if (sc.exp) {
610
+ const e = text.charCodeAt(j);
611
+ if (e === 101 || e === 69) {
612
+ let k = j + 1;
613
+ const s = text.charCodeAt(k);
614
+ if (s === 43 || s === 45) k++;
615
+ const end = skipDigits(text, k, length);
616
+ if (end > k) j = end;
617
+ }
618
+ }
619
+ return j;
620
+ }
621
+ case STRING: {
622
+ const q2 = sc.quote;
623
+ if (text.charCodeAt(offset) !== q2) return -1;
624
+ let j = offset + 1;
625
+ for (; ; ) {
626
+ if (j >= length) return -1;
627
+ const c = text.charCodeAt(j);
628
+ if (c === q2) return j + 1;
629
+ if (c === 92) {
630
+ const d = text.charCodeAt(j + 1);
631
+ if (sc.escAny) {
632
+ if (!(d >= 0) || d === 10 || d === 13 || d === 8232 || d === 8233) return -1;
633
+ j += 2;
634
+ } else if (d < 128 && sc.esc[d] === 1) {
635
+ j += 2;
636
+ } 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))) {
637
+ j += 6;
638
+ } else {
639
+ return -1;
640
+ }
641
+ continue;
642
+ }
643
+ if (sc.ctrl && c < 32) return -1;
644
+ j++;
645
+ }
646
+ }
647
+ default: {
648
+ const q2 = sc.quote;
649
+ if (text.charCodeAt(offset) !== q2) return -1;
650
+ const quote2 = String.fromCharCode(q2);
651
+ let fallback = -1;
652
+ let j = offset + 1;
653
+ for (; ; ) {
654
+ j = text.indexOf(quote2, j);
655
+ if (j === -1) return fallback;
656
+ if (text.charCodeAt(j + 1) !== q2) return j + 1;
657
+ fallback = j + 1;
658
+ j += 2;
659
+ }
660
+ }
661
+ }
662
+ }
663
+
664
+ // src/state.js
665
+ var FAIL = /* @__PURE__ */ Symbol("grammatik.fail");
666
+ var memoTables = (slots, length) => {
667
+ if (!slots) return null;
668
+ const tables = new Array(slots);
669
+ for (let i = 0; i < slots; i++) tables[i] = { ends: new Int32Array(length + 1), vals: new Array(length + 1) };
670
+ return tables;
671
+ };
672
+ var createState = (tokens, grammar, options = {}) => ({
673
+ tokens,
674
+ idx: 0,
675
+ maxIdx: 0,
676
+ expected: [],
677
+ grammar,
678
+ memo: options.memo ? /* @__PURE__ */ new Map() : null,
679
+ rm: memoTables(options.memoSlots, tokens.length),
680
+ // Entering a rule is one array store rather than an object allocation, and
681
+ // the stack is snapshotted only on a furthest failure — by the time an error
682
+ // is built it would otherwise have unwound to just the start rule.
683
+ ruleNames: [],
684
+ ruleDepth: 0,
685
+ failureStack: null,
686
+ track: options.track === true,
687
+ steps: 0,
688
+ maxSteps: options.maxSteps || 0
689
+ });
690
+ function expect(state, expectation) {
691
+ if (state.idx > state.maxIdx) {
692
+ state.maxIdx = state.idx;
693
+ if (!state.track) return;
694
+ state.expected = [];
695
+ state.failureStack = state.ruleNames.slice(0, state.ruleDepth);
696
+ }
697
+ if (!state.track || state.idx !== state.maxIdx) return;
698
+ if (!state.expected.includes(expectation)) state.expected.push(expectation);
699
+ }
369
700
  var LexError = class extends Error {
370
701
  /**
371
702
  * @param {string} message
@@ -379,6 +710,33 @@ var LexError = class extends Error {
379
710
  this.col = position.col;
380
711
  }
381
712
  };
713
+ var ParseError = class extends Error {
714
+ /**
715
+ * @param {string} message
716
+ * @param {{ token: import('./lexer.js').Token | null, expected: string[], ruleStack: string[] }} details
717
+ */
718
+ constructor(message, details) {
719
+ super(message);
720
+ this.name = "ParseError";
721
+ this.token = details.token;
722
+ this.expected = details.expected;
723
+ this.ruleStack = details.ruleStack;
724
+ }
725
+ };
726
+ var describeExpected = (expected) => expected.length === 0 ? "nothing" : expected.length === 1 ? expected[0] : `one of [${expected.join(", ")}]`;
727
+ function buildParseError(state) {
728
+ const token = state.tokens[state.maxIdx] || null;
729
+ const expected = [...state.expected].sort();
730
+ const ruleStack = state.failureStack || state.ruleNames.slice(0, state.ruleDepth);
731
+ const at = (t) => state.positions === "offset" ? `offset ${t.start}` : `line ${t.line} column ${t.col}`;
732
+ const found = token ? `--> ${token.image} <-- (${token.type}) at ${at(token)}` : "end of input";
733
+ return new ParseError(
734
+ `Expecting ${describeExpected(expected)} but found ${found}`,
735
+ { token, expected, ruleStack }
736
+ );
737
+ }
738
+
739
+ // src/lexer.js
382
740
  var DEFAULT_MODE = "default";
383
741
  function modesOf(def) {
384
742
  if (Array.isArray(def.modes) && def.modes.length > 0) return def.modes;
@@ -388,6 +746,8 @@ function modesOf(def) {
388
746
  function createLexer(defs, options = {}) {
389
747
  const positions = options.positions === "offset" ? "offset" : "full";
390
748
  const trackLines = positions === "full";
749
+ const useScanners = options.scanners !== false;
750
+ const useFuse = options.fuse !== false;
391
751
  if (!Array.isArray(defs) || defs.length === 0) throw new Error("createLexer: expected a non-empty array of token definitions");
392
752
  const tokenNames = defs.map((d) => d.name);
393
753
  const seen = /* @__PURE__ */ new Set();
@@ -407,7 +767,7 @@ function createLexer(defs, options = {}) {
407
767
  return id;
408
768
  };
409
769
  for (const def of defs) for (const mode of modesOf(def)) idOf(mode);
410
- const compiled = defs.map((def) => {
770
+ const compiled = defs.map((def, id) => {
411
771
  if (!def.name) throw new Error("createLexer: every token definition needs a name");
412
772
  if (!def.pattern && !def.literal) throw new Error(`createLexer: token '${def.name}' needs a pattern or a literal`);
413
773
  if (def.pattern && def.literal) throw new Error(`createLexer: token '${def.name}' has both a pattern and a literal; pick one`);
@@ -422,13 +782,24 @@ function createLexer(defs, options = {}) {
422
782
  }
423
783
  return {
424
784
  name: def.name,
785
+ // The declaration index. `createParser` derives the identical mapping
786
+ // from `tokenNames` independently (same array, same order), so a
787
+ // combinator that resolves a `consume` target's id at build time agrees
788
+ // with what a token actually carries without the two ever comparing notes.
789
+ id,
425
790
  skip: !!def.skip,
426
791
  literal: def.literal,
427
792
  longerAlt: def.longerAlt || null,
428
793
  regex: def.pattern ? new RegExp(def.pattern, def.ignoreCase ? "iy" : "y") : null,
794
+ // A table-driven stand-in for the regex where its shape is recognised;
795
+ // null otherwise (and always, with `scanners: false`). It is a field on
796
+ // every definition so they all keep one shape. See scan.js.
797
+ scanner: useScanners && def.pattern ? buildScanner(def.pattern, !!def.ignoreCase) : null,
429
798
  modes: modesOf(def),
430
799
  pushMode: def.pushMode ? idOf(def.pushMode) : -1,
431
- popMode: !!def.popMode
800
+ popMode: !!def.popMode,
801
+ // Set only on the stand-in `fuse` builds for a run of regexes; see there.
802
+ members: null
432
803
  };
433
804
  });
434
805
  const byName = new Map(compiled.map((c) => [c.name, c]));
@@ -445,14 +816,47 @@ function createLexer(defs, options = {}) {
445
816
  for (const c of compiled) {
446
817
  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);
447
818
  }
448
- const modes = modeNames.map((name) => {
819
+ const FUSE_MIN = 3;
820
+ 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.
821
+ new RegExp(def.regex.source + "|").exec("").length === 1;
822
+ function fuse(list) {
823
+ if (!useFuse) return list;
824
+ const out = [];
825
+ for (let i = 0; i < list.length; ) {
826
+ let j = i;
827
+ while (j < list.length && fusible(list[j]) && list[j].regex.flags === list[i].regex.flags) j++;
828
+ if (j - i < FUSE_MIN) {
829
+ out.push(list[i++]);
830
+ continue;
831
+ }
832
+ const run = list.slice(i, j);
833
+ out.push({
834
+ name: run.map((d) => d.name).join("|"),
835
+ id: -1,
836
+ skip: false,
837
+ literal: void 0,
838
+ longerAlt: null,
839
+ longerAltDef: void 0,
840
+ regex: new RegExp(run.map((d) => `(${d.regex.source})`).join("|"), run[0].regex.flags),
841
+ scanner: null,
842
+ modes: run[0].modes,
843
+ pushMode: -1,
844
+ popMode: false,
845
+ members: run
846
+ });
847
+ i = j;
848
+ }
849
+ return out;
850
+ }
851
+ const modes = modeNames.map((name, id) => {
449
852
  const members = compiled.filter((c) => c.modes.includes(name));
450
853
  const alwaysTry = members.filter((c) => c.firstChars === null);
451
- const candidatesFor = (code) => members.filter((c) => c.firstChars === null || c.firstChars.has(code));
854
+ const candidatesFor = (code) => fuse(members.filter((c) => c.firstChars === null || c.firstChars.has(code)));
452
855
  const ascii = new Array(128);
453
856
  for (let code = 0; code < 128; code++) ascii[code] = candidatesFor(code);
454
857
  return {
455
858
  name,
859
+ id,
456
860
  members,
457
861
  ascii,
458
862
  wide: /* @__PURE__ */ new Map(),
@@ -476,6 +880,30 @@ function createLexer(defs, options = {}) {
476
880
  const multiMode = modes.length > 1 || compiled.some((c) => c.popMode || c.pushMode !== -1);
477
881
  const namesModes = modes.length > 1;
478
882
  const initial = modes[0];
883
+ let fusedMember = null;
884
+ let fusedText = "";
885
+ function fusedEnd(run, text, offset) {
886
+ run.regex.lastIndex = offset;
887
+ const match = run.regex.exec(text);
888
+ if (match === null) return -1;
889
+ if (match[0].length === 0) {
890
+ for (const member of run.members) {
891
+ member.regex.lastIndex = offset;
892
+ const one = member.regex.exec(text);
893
+ if (one !== null && one[0].length > 0) {
894
+ fusedMember = member;
895
+ fusedText = one[0];
896
+ return offset + fusedText.length;
897
+ }
898
+ }
899
+ return -1;
900
+ }
901
+ let group = 1;
902
+ while (match[group] === void 0) group++;
903
+ fusedMember = run.members[group - 1];
904
+ fusedText = match[0];
905
+ return offset + fusedText.length;
906
+ }
479
907
  function longerAltEnd(def, text, offset) {
480
908
  const alt2 = def.longerAltDef;
481
909
  if (alt2.literal !== void 0) return text.startsWith(alt2.literal, offset) ? offset + alt2.literal.length : -1;
@@ -495,19 +923,30 @@ function createLexer(defs, options = {}) {
495
923
  outer: while (offset < length) {
496
924
  const applicable = mode.dispatch ? candidates(mode, text.charCodeAt(offset)) : mode.members;
497
925
  for (let i = 0; i < applicable.length; i++) {
498
- const def = applicable[i];
926
+ let def = applicable[i];
499
927
  let end = -1;
500
928
  let matched = null;
501
- if (def.literal !== void 0) {
929
+ if (def.members !== null) {
930
+ end = fusedEnd(def, text, offset);
931
+ if (end === -1) continue;
932
+ def = fusedMember;
933
+ matched = fusedText;
934
+ } else if (def.literal !== void 0) {
502
935
  if (text.startsWith(def.literal, offset)) end = offset + def.literal.length;
503
- } else if (def.skip) {
504
- def.regex.lastIndex = offset;
505
- if (def.regex.test(text) && def.regex.lastIndex > offset) end = def.regex.lastIndex;
506
936
  } else {
507
- def.regex.lastIndex = offset;
508
- const match = def.regex.exec(text);
509
- if (match !== null && match[0].length > 0) matched = match[0];
510
- if (matched !== null) end = offset + matched.length;
937
+ end = def.scanner === null ? DEFER : scanEnd(def.scanner, text, offset, length);
938
+ if (end === DEFER) {
939
+ end = -1;
940
+ if (def.skip) {
941
+ def.regex.lastIndex = offset;
942
+ if (def.regex.test(text) && def.regex.lastIndex > offset) end = def.regex.lastIndex;
943
+ } else {
944
+ def.regex.lastIndex = offset;
945
+ const match = def.regex.exec(text);
946
+ if (match !== null && match[0].length > 0) matched = match[0];
947
+ if (matched !== null) end = offset + matched.length;
948
+ }
949
+ }
511
950
  }
512
951
  if (end === -1) continue;
513
952
  let winner = def;
@@ -520,14 +959,7 @@ function createLexer(defs, options = {}) {
520
959
  }
521
960
  if (!winner.skip) {
522
961
  const image = winner.literal !== void 0 ? winner.literal : winner === def && matched !== null ? matched : text.slice(offset, end);
523
- tokens.push({
524
- type: winner.name,
525
- image,
526
- start: offset,
527
- end,
528
- line: trackLines ? line : 0,
529
- col: trackLines ? offset - lineStart + 1 : 0
530
- });
962
+ 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 });
531
963
  }
532
964
  if (trackLines) {
533
965
  for (let j = offset; j < end; j++) {
@@ -563,62 +995,238 @@ function createLexer(defs, options = {}) {
563
995
  }
564
996
  const inMode = (mode) => namesModes ? ` (in lexer mode '${mode.name}')` : "";
565
997
  const describe = (offset, line, lineStart) => trackLines ? `line ${line} column ${offset - lineStart + 1}` : `offset ${offset}`;
566
- return { tokenize, tokenNames, positions, modeNames };
567
- }
568
-
569
- // src/state.js
570
- var FAIL = /* @__PURE__ */ Symbol("grammatik.fail");
571
- var createState = (tokens, grammar, options = {}) => ({
572
- tokens,
573
- idx: 0,
574
- maxIdx: 0,
575
- expected: [],
576
- grammar,
577
- memo: options.memo ? /* @__PURE__ */ new Map() : null,
578
- // Entering a rule is one array store rather than an object allocation, and
579
- // the stack is snapshotted only on a furthest failure — by the time an error
580
- // is built it would otherwise have unwound to just the start rule.
581
- ruleNames: [],
582
- ruleDepth: 0,
583
- failureStack: null,
584
- track: options.track === true,
585
- steps: 0,
586
- maxSteps: options.maxSteps || 0
587
- });
588
- function expect(state, expectation) {
589
- if (state.idx > state.maxIdx) {
590
- state.maxIdx = state.idx;
591
- if (!state.track) return;
592
- state.expected = [];
593
- state.failureStack = state.ruleNames.slice(0, state.ruleDepth);
998
+ if (!options.incremental) return { tokenize, tokenNames, positions, modeNames };
999
+ const recover = !!options.recover;
1000
+ const errorTokenName = options.errorToken || "Error";
1001
+ function snapshotStack(stack, depth) {
1002
+ const ids = new Array(depth);
1003
+ for (let i = 0; i < depth; i++) ids[i] = stack[i].id;
1004
+ return ids;
594
1005
  }
595
- if (!state.track || state.idx !== state.maxIdx) return;
596
- if (!state.expected.includes(expectation)) state.expected.push(expectation);
597
- }
598
- var ParseError = class extends Error {
599
- /**
600
- * @param {string} message
601
- * @param {{ token: import('./lexer.js').Token | null, expected: string[], ruleStack: string[] }} details
602
- */
603
- constructor(message, details) {
604
- super(message);
605
- this.name = "ParseError";
606
- this.token = details.token;
607
- this.expected = details.expected;
608
- this.ruleStack = details.ruleStack;
1006
+ function sameStack(a, b) {
1007
+ if (a.length !== b.length) return false;
1008
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1009
+ return true;
609
1010
  }
610
- };
611
- var describeExpected = (expected) => expected.length === 0 ? "nothing" : expected.length === 1 ? expected[0] : `one of [${expected.join(", ")}]`;
612
- function buildParseError(state) {
613
- const token = state.tokens[state.maxIdx] || null;
614
- const expected = [...state.expected].sort();
615
- const ruleStack = state.failureStack || state.ruleNames.slice(0, state.ruleDepth);
616
- const at = (t) => state.positions === "offset" ? `offset ${t.start}` : `line ${t.line} column ${t.col}`;
617
- const found = token ? `--> ${token.image} <-- (${token.type}) at ${at(token)}` : "end of input";
618
- return new ParseError(
619
- `Expecting ${describeExpected(expected)} but found ${found}`,
620
- { token, expected, ruleStack }
621
- );
1011
+ function scan(text, offset, mode, stack, depth, line, lineStart) {
1012
+ const tokens = [];
1013
+ const checkpoints = [];
1014
+ const length = text.length;
1015
+ outer: while (offset < length) {
1016
+ checkpoints.push({ modeId: mode.id, stack: snapshotStack(stack, depth), line, lineStart, offset });
1017
+ const applicable = mode.dispatch ? candidates(mode, text.charCodeAt(offset)) : mode.members;
1018
+ for (let i = 0; i < applicable.length; i++) {
1019
+ let def = applicable[i];
1020
+ let end = -1;
1021
+ let matched = null;
1022
+ if (def.members !== null) {
1023
+ end = fusedEnd(def, text, offset);
1024
+ if (end === -1) continue;
1025
+ def = fusedMember;
1026
+ matched = fusedText;
1027
+ } else if (def.literal !== void 0) {
1028
+ if (text.startsWith(def.literal, offset)) end = offset + def.literal.length;
1029
+ } else {
1030
+ end = def.scanner === null ? DEFER : scanEnd(def.scanner, text, offset, length);
1031
+ if (end === DEFER) {
1032
+ end = -1;
1033
+ if (def.skip) {
1034
+ def.regex.lastIndex = offset;
1035
+ if (def.regex.test(text) && def.regex.lastIndex > offset) end = def.regex.lastIndex;
1036
+ } else {
1037
+ def.regex.lastIndex = offset;
1038
+ const match = def.regex.exec(text);
1039
+ if (match !== null && match[0].length > 0) matched = match[0];
1040
+ if (matched !== null) end = offset + matched.length;
1041
+ }
1042
+ }
1043
+ }
1044
+ if (end === -1) continue;
1045
+ let winner = def;
1046
+ if (def.longerAltDef) {
1047
+ const altEnd = longerAltEnd(def, text, offset);
1048
+ if (altEnd > end) {
1049
+ end = altEnd;
1050
+ winner = def.longerAltDef;
1051
+ }
1052
+ }
1053
+ if (!winner.skip) {
1054
+ const image = winner.literal !== void 0 ? winner.literal : winner === def && matched !== null ? matched : text.slice(offset, end);
1055
+ 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 });
1056
+ }
1057
+ if (trackLines) {
1058
+ for (let j = offset; j < end; j++) {
1059
+ if (text.charCodeAt(j) !== 10) continue;
1060
+ line++;
1061
+ lineStart = j + 1;
1062
+ }
1063
+ }
1064
+ if (multiMode) {
1065
+ if (winner.popMode) {
1066
+ if (depth === 0) {
1067
+ if (!recover) {
1068
+ throw new LexError(
1069
+ `Token '${winner.name}' popped the initial lexer mode at ${describe(offset, line, lineStart)}`,
1070
+ { offset, line: trackLines ? line : 0, col: trackLines ? offset - lineStart + 1 : 0 }
1071
+ );
1072
+ }
1073
+ } else {
1074
+ mode = stack[--depth];
1075
+ }
1076
+ } else if (winner.pushMode !== -1) {
1077
+ stack[depth++] = mode;
1078
+ mode = modes[winner.pushMode];
1079
+ }
1080
+ }
1081
+ offset = end;
1082
+ continue outer;
1083
+ }
1084
+ if (recover) {
1085
+ 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 });
1086
+ if (trackLines && text.charCodeAt(offset) === 10) {
1087
+ line++;
1088
+ lineStart = offset + 1;
1089
+ }
1090
+ offset++;
1091
+ continue outer;
1092
+ }
1093
+ const column = offset - lineStart + 1;
1094
+ throw new LexError(
1095
+ trackLines ? `Unexpected character '${text[offset]}' at line ${line} column ${column}${inMode(mode)}` : `Unexpected character '${text[offset]}' at offset ${offset}${inMode(mode)}`,
1096
+ { offset, line: trackLines ? line : 0, col: trackLines ? column : 0 }
1097
+ );
1098
+ }
1099
+ return { tokens, checkpoints, mode, stack, depth, line, lineStart };
1100
+ }
1101
+ function tokenizeIncremental(text) {
1102
+ if (typeof text !== "string") throw new TypeError("tokenizeIncremental: expected a string");
1103
+ const result = scan(text, 0, initial, multiMode ? [] : [], 0, 1, 0);
1104
+ 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 } };
1105
+ }
1106
+ function tokenFloor(tokens, offset) {
1107
+ let lo = 0;
1108
+ let hi = tokens.length;
1109
+ while (lo < hi) {
1110
+ const mid = lo + hi >>> 1;
1111
+ if (tokens[mid].start < offset) lo = mid + 1;
1112
+ else hi = mid;
1113
+ }
1114
+ return lo;
1115
+ }
1116
+ function checkpointFloor(checkpoints, eof, offset) {
1117
+ let lo = 0;
1118
+ let hi = checkpoints.length + 1;
1119
+ while (lo < hi) {
1120
+ const mid = lo + hi >>> 1;
1121
+ const at = mid < checkpoints.length ? checkpoints[mid].offset : eof.offset;
1122
+ if (at <= offset) lo = mid + 1;
1123
+ else hi = mid;
1124
+ }
1125
+ return lo - 1;
1126
+ }
1127
+ function checkpointAt(checkpoints, eof, offset) {
1128
+ if (eof.offset === offset) return eof;
1129
+ let lo = 0;
1130
+ let hi = checkpoints.length;
1131
+ while (lo < hi) {
1132
+ const mid = lo + hi >>> 1;
1133
+ if (checkpoints[mid].offset < offset) lo = mid + 1;
1134
+ else hi = mid;
1135
+ }
1136
+ return lo < checkpoints.length && checkpoints[lo].offset === offset ? checkpoints[lo] : null;
1137
+ }
1138
+ function relex(newText, prev, edit) {
1139
+ if (typeof newText !== "string") throw new TypeError("relex: expected a string");
1140
+ const tokens = prev.tokens;
1141
+ const checkpoints = prev.checkpoints;
1142
+ const delta = edit.newEnd - edit.oldEnd;
1143
+ let resumeIndex = checkpointFloor(checkpoints, prev.eof, edit.start);
1144
+ if (resumeIndex > 0) resumeIndex--;
1145
+ let boundary = resumeIndex < checkpoints.length ? checkpoints[resumeIndex].offset : prev.eof.offset;
1146
+ let ti = tokenFloor(tokens, boundary);
1147
+ while (ti > 0 && tokens[ti - 1].end === boundary) {
1148
+ ti--;
1149
+ boundary = tokens[ti].start;
1150
+ }
1151
+ if (boundary < (resumeIndex < checkpoints.length ? checkpoints[resumeIndex].offset : prev.eof.offset)) {
1152
+ resumeIndex = checkpointFloor(checkpoints, prev.eof, boundary);
1153
+ }
1154
+ const resume = resumeIndex < checkpoints.length ? checkpoints[resumeIndex] : prev.eof;
1155
+ const resumeMode = modes[resume.modeId];
1156
+ const resumeStack = resume.stack.map((id) => modes[id]);
1157
+ const scanned = scan(newText, resume.offset, resumeMode, resumeStack, resumeStack.length, resume.line, resume.lineStart);
1158
+ const scannedEof = { modeId: scanned.mode.id, stack: snapshotStack(scanned.stack, scanned.depth), line: scanned.line, lineStart: scanned.lineStart, offset: newText.length };
1159
+ const VERIFY_WINDOW = 4;
1160
+ function convergenceVerified(oldOffset, newOffset) {
1161
+ const oldStart = tokenFloor(tokens, oldOffset);
1162
+ const freshStart = tokenFloor(scanned.tokens, newOffset);
1163
+ for (let k = 0; k < VERIFY_WINDOW; k++) {
1164
+ const oldTok = tokens[oldStart + k];
1165
+ const freshTok = scanned.tokens[freshStart + k];
1166
+ if (oldTok === void 0 && freshTok === void 0) return true;
1167
+ if (oldTok === void 0 || freshTok === void 0) return false;
1168
+ if (oldTok.type !== freshTok.type || oldTok.image !== freshTok.image) return false;
1169
+ }
1170
+ return true;
1171
+ }
1172
+ let convergeNewOffset = -1;
1173
+ let convergeOldOffset = -1;
1174
+ let convergeOldCheckpoint = null;
1175
+ let freshCheckpointCount = 0;
1176
+ for (let s = 0; s <= scanned.checkpoints.length; s++) {
1177
+ const cp = s < scanned.checkpoints.length ? scanned.checkpoints[s] : scannedEof;
1178
+ if (cp.offset < edit.newEnd) continue;
1179
+ const oldOffset = cp.offset - delta;
1180
+ const oldCp = checkpointAt(checkpoints, prev.eof, oldOffset);
1181
+ if (oldCp && oldCp.modeId === cp.modeId && sameStack(oldCp.stack, cp.stack) && convergenceVerified(oldOffset, cp.offset)) {
1182
+ convergeNewOffset = cp.offset;
1183
+ convergeOldOffset = oldOffset;
1184
+ convergeOldCheckpoint = oldCp;
1185
+ freshCheckpointCount = s;
1186
+ break;
1187
+ }
1188
+ }
1189
+ const tokenFrom = tokenFloor(tokens, resume.offset);
1190
+ const checkpointFrom = resumeIndex;
1191
+ if (convergeNewOffset === -1) {
1192
+ tokens.splice(tokenFrom, tokens.length - tokenFrom, ...scanned.tokens);
1193
+ checkpoints.splice(checkpointFrom, checkpoints.length - checkpointFrom, ...scanned.checkpoints);
1194
+ prev.text = newText;
1195
+ prev.eof = scannedEof;
1196
+ return prev;
1197
+ }
1198
+ const freshTokenCount = tokenFloor(scanned.tokens, convergeNewOffset);
1199
+ const lineDelta = trackLines ? scanned.checkpoints.length > freshCheckpointCount ? scanned.checkpoints[freshCheckpointCount].line - convergeOldCheckpoint.line : scannedEof.line - convergeOldCheckpoint.line : 0;
1200
+ const boundaryOldLineStart = convergeOldCheckpoint.lineStart;
1201
+ const boundaryNewLineStart = (freshCheckpointCount < scanned.checkpoints.length ? scanned.checkpoints[freshCheckpointCount] : scannedEof).lineStart;
1202
+ const shiftedLineStart = (oldLineStart) => oldLineStart === boundaryOldLineStart ? boundaryNewLineStart : oldLineStart + delta;
1203
+ const tailTokenFrom = tokenFloor(tokens, convergeOldOffset);
1204
+ for (let i = tailTokenFrom; i < tokens.length; i++) {
1205
+ const t = tokens[i];
1206
+ if (trackLines) {
1207
+ const oldLineStart = t.start - (t.col - 1);
1208
+ t.col = t.start + delta - shiftedLineStart(oldLineStart) + 1;
1209
+ t.line += lineDelta;
1210
+ }
1211
+ t.start += delta;
1212
+ t.end += delta;
1213
+ }
1214
+ tokens.splice(tokenFrom, tailTokenFrom - tokenFrom, ...scanned.tokens.slice(0, freshTokenCount));
1215
+ const tailCheckpointFrom = checkpointFloor(checkpoints, prev.eof, convergeOldOffset - 1) + 1;
1216
+ for (let i = tailCheckpointFrom; i < checkpoints.length; i++) {
1217
+ const cp = checkpoints[i];
1218
+ cp.lineStart = shiftedLineStart(cp.lineStart);
1219
+ cp.line += lineDelta;
1220
+ cp.offset += delta;
1221
+ }
1222
+ checkpoints.splice(checkpointFrom, tailCheckpointFrom - checkpointFrom, ...scanned.checkpoints.slice(0, freshCheckpointCount));
1223
+ prev.text = newText;
1224
+ prev.eof.lineStart = shiftedLineStart(prev.eof.lineStart);
1225
+ prev.eof.line += lineDelta;
1226
+ prev.eof.offset += delta;
1227
+ return prev;
1228
+ }
1229
+ return { tokenize, tokenizeIncremental, relex, tokenNames, positions, modeNames };
622
1230
  }
623
1231
 
624
1232
  // src/analyze.js
@@ -651,6 +1259,7 @@ var PARSER_OPS = /* @__PURE__ */ new Set([
651
1259
  "many",
652
1260
  "many1",
653
1261
  "option",
1262
+ "flag",
654
1263
  "manySep",
655
1264
  "many1Sep",
656
1265
  "infixLeft",
@@ -658,6 +1267,8 @@ var PARSER_OPS = /* @__PURE__ */ new Set([
658
1267
  "postfix",
659
1268
  "prefix",
660
1269
  "between",
1270
+ "pick",
1271
+ "yields",
661
1272
  "as",
662
1273
  "oneOf",
663
1274
  "eof",
@@ -711,7 +1322,8 @@ function firstOf(node, ruleFirsts) {
711
1322
  return result;
712
1323
  }
713
1324
  case "many":
714
- case "option": {
1325
+ case "option":
1326
+ case "flag": {
715
1327
  const inner = firstOf(arg, ruleFirsts);
716
1328
  return { tokens: inner.tokens, nullable: true, unknown: inner.unknown };
717
1329
  }
@@ -734,6 +1346,9 @@ function firstOf(node, ruleFirsts) {
734
1346
  return firstOf(Array.isArray(arg) ? arg[0] : arg && arg.parser, ruleFirsts);
735
1347
  case "between":
736
1348
  return firstOf({ seq: [arg && arg.open, arg && arg.body, arg && arg.close] }, ruleFirsts);
1349
+ case "pick":
1350
+ case "yields":
1351
+ return firstOf({ seq: arg && Array.isArray(arg.parsers) ? arg.parsers : [] }, ruleFirsts);
737
1352
  case "prefix": {
738
1353
  const ops = firstOf(arg && arg.operator, ruleFirsts);
739
1354
  const inner = firstOf(arg && arg.operand, ruleFirsts);
@@ -797,12 +1412,94 @@ function recognizeSeqIdiom(children) {
797
1412
  if (rewritten !== BAIL) return { as: [parser, rewritten] };
798
1413
  }
799
1414
  }
1415
+ if (children.length >= 2) {
1416
+ const last = children[children.length - 1];
1417
+ const head = children.slice(0, -1);
1418
+ if (opOf(last) === "action" && isConstant(last.action) && head.every(isPlainParser) && !head.some(usesOuterScope)) {
1419
+ return { yields: { parsers: head, value: last.action } };
1420
+ }
1421
+ if (opOf(last) === "action" && opOf(last.action) === "val" && typeof last.action.val === "string") {
1422
+ const name = last.action.val;
1423
+ const labelled = head.map((child, i) => opOf(child) === "label" ? i : -1).filter((i) => i !== -1);
1424
+ if (labelled.length === 1 && head[labelled[0]].label[0] === name && head.every((child, i) => i === labelled[0] || isPlainParser(child)) && !head.some(usesOuterScope)) {
1425
+ return { pick: { parsers: head.map((child, i) => i === labelled[0] ? child.label[1] : child), index: labelled[0] } };
1426
+ }
1427
+ }
1428
+ }
800
1429
  return null;
801
1430
  }
1431
+ var isConstant = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1432
+ function usesOuterScope(logic) {
1433
+ if (Array.isArray(logic)) return logic.some(usesOuterScope);
1434
+ if (!logic || typeof logic !== "object") return false;
1435
+ for (const key of Object.keys(logic)) {
1436
+ const arg = logic[key];
1437
+ if (key === "val" && Array.isArray(arg) && Array.isArray(arg[0])) return true;
1438
+ if (key === "var") {
1439
+ if (typeof arg === "string" && arg.includes("../")) return true;
1440
+ if (Array.isArray(arg) && typeof arg[0] === "string" && arg[0].includes("../")) return true;
1441
+ }
1442
+ if (usesOuterScope(arg)) return true;
1443
+ }
1444
+ return false;
1445
+ }
802
1446
  function isPlainParser(node) {
803
1447
  const op = opOf(node);
804
1448
  return op !== null && op !== "label" && op !== "action";
805
1449
  }
1450
+ function deepEqual(a, b) {
1451
+ if (a === b) return true;
1452
+ if (Array.isArray(a) || Array.isArray(b)) {
1453
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
1454
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
1455
+ return true;
1456
+ }
1457
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
1458
+ const keysA = Object.keys(a);
1459
+ const keysB = Object.keys(b);
1460
+ if (keysA.length !== keysB.length) return false;
1461
+ for (const key of keysA) {
1462
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false;
1463
+ }
1464
+ return true;
1465
+ }
1466
+ function recognizeAltPrefixIdiom(branches) {
1467
+ if (!Array.isArray(branches) || branches.length < 2) return null;
1468
+ if (!branches.every((b) => opOf(b) === "seq" && Array.isArray(b.seq))) return null;
1469
+ const seqs = branches.map((b) => b.seq);
1470
+ const minLen = Math.min(...seqs.map((s) => s.length));
1471
+ let n = 0;
1472
+ while (n < minLen) {
1473
+ const candidate = seqs[0][n];
1474
+ const op = opOf(candidate);
1475
+ if (op === "label" || op === "action") break;
1476
+ if (!seqs.every((s) => deepEqual(s[n], candidate))) break;
1477
+ n++;
1478
+ }
1479
+ if (n === 0) return null;
1480
+ if (!seqs.every((s) => s.slice(n).some((child) => opOf(child) === "action"))) return null;
1481
+ const prefix = seqs[0].slice(0, n);
1482
+ const remainders = seqs.map((s) => ({ seq: s.slice(n) }));
1483
+ return {
1484
+ seq: [
1485
+ ...prefix,
1486
+ { label: ["__grammatik_rest", { alt: remainders }] },
1487
+ { action: { val: "__grammatik_rest" } }
1488
+ ]
1489
+ };
1490
+ }
1491
+ function recognizeAltConsumeIdiom(branches) {
1492
+ if (!Array.isArray(branches) || branches.length < 2) return null;
1493
+ const names = [];
1494
+ for (const branch of branches) {
1495
+ if (opOf(branch) !== "consume") return null;
1496
+ const arg = branch.consume;
1497
+ const name = typeof arg === "string" ? arg : Array.isArray(arg) && arg.length === 1 ? arg[0] : null;
1498
+ if (typeof name !== "string") return null;
1499
+ names.push(name);
1500
+ }
1501
+ return { oneOf: names };
1502
+ }
806
1503
  var BAIL = /* @__PURE__ */ Symbol("grammatik.bail");
807
1504
  function rebaseOnLabel(logic, name) {
808
1505
  if (Array.isArray(logic)) {
@@ -869,6 +1566,7 @@ function leftmostCalls(node, ruleFirsts, out) {
869
1566
  case "many":
870
1567
  case "many1":
871
1568
  case "option":
1569
+ case "flag":
872
1570
  case "lookahead":
873
1571
  case "negLookahead":
874
1572
  leftmostCalls(arg, ruleFirsts, out);
@@ -879,6 +1577,10 @@ function leftmostCalls(node, ruleFirsts, out) {
879
1577
  case "between":
880
1578
  if (arg) leftmostCalls({ seq: [arg.open, arg.body, arg.close] }, ruleFirsts, out);
881
1579
  break;
1580
+ case "pick":
1581
+ case "yields":
1582
+ if (arg && Array.isArray(arg.parsers)) leftmostCalls({ seq: arg.parsers }, ruleFirsts, out);
1583
+ break;
882
1584
  case "prefix":
883
1585
  if (arg) {
884
1586
  leftmostCalls(arg.operator, ruleFirsts, out);
@@ -945,6 +1647,7 @@ function validateNode(node, grammar, tokenNames, errors, path) {
945
1647
  case "many":
946
1648
  case "many1":
947
1649
  case "option":
1650
+ case "flag":
948
1651
  case "text":
949
1652
  case "lookahead":
950
1653
  case "negLookahead":
@@ -966,6 +1669,26 @@ function validateNode(node, grammar, tokenNames, errors, path) {
966
1669
  }
967
1670
  break;
968
1671
  }
1672
+ case "pick": {
1673
+ if (!arg || !Array.isArray(arg.parsers) || arg.parsers.length === 0) {
1674
+ errors.push(`${path}: 'pick' expects { parsers: [parser, ...], index }`);
1675
+ } else if (!Number.isInteger(arg.index) || arg.index < 0 || arg.index >= arg.parsers.length) {
1676
+ errors.push(`${path}: 'pick' index ${JSON.stringify(arg.index)} is out of range for ${arg.parsers.length} parser(s)`);
1677
+ } else {
1678
+ arg.parsers.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.pick[${i}]`));
1679
+ }
1680
+ break;
1681
+ }
1682
+ case "yields": {
1683
+ if (!arg || !Array.isArray(arg.parsers) || arg.parsers.length === 0 || !("value" in arg)) {
1684
+ errors.push(`${path}: 'yields' expects { parsers: [parser, ...], value }`);
1685
+ } else if (!isConstant(arg.value)) {
1686
+ errors.push(`${path}: 'yields' value must be a string, number, boolean or null, got ${JSON.stringify(arg.value)}`);
1687
+ } else {
1688
+ arg.parsers.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.yields[${i}]`));
1689
+ }
1690
+ break;
1691
+ }
969
1692
  case "prefix": {
970
1693
  if (!arg || typeof arg !== "object" || !("operator" in arg) || !("operand" in arg) || !("combine" in arg)) {
971
1694
  errors.push(`${path}: 'prefix' expects { operator, operand, combine }`);
@@ -1052,16 +1775,38 @@ function analyze(grammar, options = {}) {
1052
1775
  return { firsts, errors, leftRecursive };
1053
1776
  }
1054
1777
  function buildDispatch(branches, firsts) {
1055
- const table = /* @__PURE__ */ new Map();
1056
- for (const branch of branches) {
1057
- const first = firstOf(branch, firsts);
1778
+ const perBranch = branches.map((branch) => firstOf(branch, firsts));
1779
+ for (const first of perBranch) {
1058
1780
  if (first.nullable || first.unknown || first.tokens.size === 0) return null;
1059
- for (const token of first.tokens) {
1060
- if (table.has(token)) return null;
1061
- table.set(token, branch);
1781
+ }
1782
+ const byToken = /* @__PURE__ */ new Map();
1783
+ for (let i = 0; i < branches.length; i++) {
1784
+ for (const token of perBranch[i].tokens) {
1785
+ let list = byToken.get(token);
1786
+ if (list === void 0) {
1787
+ list = [];
1788
+ byToken.set(token, list);
1789
+ }
1790
+ list.push(branches[i]);
1791
+ }
1792
+ }
1793
+ if (byToken.size === 0) return null;
1794
+ const canonical = /* @__PURE__ */ new Map();
1795
+ const table = /* @__PURE__ */ new Map();
1796
+ for (const [token, list] of byToken) {
1797
+ if (list.length === 1) {
1798
+ table.set(token, list[0]);
1799
+ continue;
1800
+ }
1801
+ const key = list.map((branch) => branches.indexOf(branch)).join(",");
1802
+ let group = canonical.get(key);
1803
+ if (group === void 0) {
1804
+ group = list;
1805
+ canonical.set(key, group);
1062
1806
  }
1807
+ table.set(token, group);
1063
1808
  }
1064
- return table.size > 0 ? table : null;
1809
+ return table;
1065
1810
  }
1066
1811
 
1067
1812
  // src/plan.js
@@ -1070,20 +1815,6 @@ function declareSync(fn) {
1070
1815
  fn[SYNC] = true;
1071
1816
  return fn;
1072
1817
  }
1073
- function usesOuterScope(logic) {
1074
- if (Array.isArray(logic)) return logic.some(usesOuterScope);
1075
- if (!logic || typeof logic !== "object") return false;
1076
- for (const key of Object.keys(logic)) {
1077
- const arg = logic[key];
1078
- if (key === "val" && Array.isArray(arg) && Array.isArray(arg[0])) return true;
1079
- if (key === "var") {
1080
- if (typeof arg === "string" && arg.includes("../")) return true;
1081
- if (Array.isArray(arg) && typeof arg[0] === "string" && arg[0].includes("../")) return true;
1082
- }
1083
- if (usesOuterScope(arg)) return true;
1084
- }
1085
- return false;
1086
- }
1087
1818
  function mayReturnContext(logic) {
1088
1819
  if (Array.isArray(logic)) return logic.some(mayReturnContext);
1089
1820
  if (!logic || typeof logic !== "object") return false;
@@ -1096,7 +1827,7 @@ function mayReturnContext(logic) {
1096
1827
  return false;
1097
1828
  }
1098
1829
  function createPlanner(config) {
1099
- const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config;
1830
+ const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null, memoSlots = /* @__PURE__ */ new Map(), idioms = true } = config;
1100
1831
  const slots = /* @__PURE__ */ new Map();
1101
1832
  const planCache = /* @__PURE__ */ new WeakMap();
1102
1833
  const opCaches = /* @__PURE__ */ new Map();
@@ -1179,9 +1910,10 @@ function createPlanner(config) {
1179
1910
  }
1180
1911
  const buildConsume = (arg) => {
1181
1912
  const name = typeof arg === "string" ? arg : arg[0];
1913
+ const id = tokenIds ? tokenIds.get(name) : void 0;
1182
1914
  return (state) => {
1183
1915
  const token = state.tokens[state.idx];
1184
- if (token !== void 0 && token.type === name) {
1916
+ if (token !== void 0 && (token.id === id || token.id === void 0 && token.type === name)) {
1185
1917
  state.idx++;
1186
1918
  return token;
1187
1919
  }
@@ -1242,6 +1974,17 @@ function createPlanner(config) {
1242
1974
  return value;
1243
1975
  };
1244
1976
  };
1977
+ const buildFlag = (arg) => {
1978
+ const child = plan(arg);
1979
+ return (state) => {
1980
+ const start = state.idx;
1981
+ if (child(state) === FAIL) {
1982
+ state.idx = start;
1983
+ return false;
1984
+ }
1985
+ return true;
1986
+ };
1987
+ };
1245
1988
  const buildInfixLeft = (arg) => {
1246
1989
  const operand = plan(arg.operand);
1247
1990
  const operator = arg.operator === void 0 ? null : plan(arg.operator);
@@ -1277,10 +2020,11 @@ function createPlanner(config) {
1277
2020
  const buildOneOf = (arg) => {
1278
2021
  const names = Array.isArray(arg) ? arg : [arg];
1279
2022
  const set = new Set(names);
2023
+ const idSet = tokenIds ? new Set(names.map((n) => tokenIds.get(n))) : null;
1280
2024
  const count = names.length;
1281
2025
  return (state) => {
1282
2026
  const token = state.tokens[state.idx];
1283
- if (token !== void 0 && set.has(token.type)) {
2027
+ if (token !== void 0 && (idSet ? token.id !== void 0 ? idSet.has(token.id) : set.has(token.type) : set.has(token.type))) {
1284
2028
  state.idx++;
1285
2029
  return token;
1286
2030
  }
@@ -1366,6 +2110,26 @@ function createPlanner(config) {
1366
2110
  return value;
1367
2111
  };
1368
2112
  };
2113
+ const buildSequence = (arg, pickIndex, constant) => {
2114
+ const fns = arg.parsers.map(plan);
2115
+ const count = fns.length;
2116
+ return (state) => {
2117
+ tick(state);
2118
+ const start = state.idx;
2119
+ let picked;
2120
+ for (let i = 0; i < count; i++) {
2121
+ const value = fns[i](state);
2122
+ if (value === FAIL) {
2123
+ state.idx = start;
2124
+ return FAIL;
2125
+ }
2126
+ if (i === pickIndex) picked = value;
2127
+ }
2128
+ return pickIndex === -1 ? constant : picked;
2129
+ };
2130
+ };
2131
+ const buildPick = (arg) => buildSequence(arg, arg.index, void 0);
2132
+ const buildYields = (arg) => buildSequence(arg, -1, arg.value);
1369
2133
  const buildPrefix = (arg) => {
1370
2134
  const operator = plan(arg.operator);
1371
2135
  const operand = plan(arg.operand);
@@ -1449,6 +2213,31 @@ function createPlanner(config) {
1449
2213
  return value;
1450
2214
  };
1451
2215
  }
2216
+ const memoSlot = memoSlots.get(name);
2217
+ if (memoSlot !== void 0) {
2218
+ return (state) => {
2219
+ tick(state);
2220
+ const table = state.rm[memoSlot];
2221
+ const at = state.idx;
2222
+ const end = table.ends[at];
2223
+ if (end !== 0) {
2224
+ state.idx = end - 1;
2225
+ return table.vals[at];
2226
+ }
2227
+ let depth = 0;
2228
+ if (state.track) {
2229
+ depth = state.ruleDepth;
2230
+ state.ruleNames[depth] = name;
2231
+ state.ruleDepth = depth + 1;
2232
+ }
2233
+ const value = slot.fn(state);
2234
+ if (state.track) state.ruleDepth = depth;
2235
+ if (value === FAIL) state.idx = at;
2236
+ table.ends[at] = state.idx + 1;
2237
+ table.vals[at] = value;
2238
+ return value;
2239
+ };
2240
+ }
1452
2241
  return (state) => {
1453
2242
  tick(state);
1454
2243
  if (!state.track) return slot.fn(state);
@@ -1527,6 +2316,9 @@ function createPlanner(config) {
1527
2316
  infixRight: buildInfixRight,
1528
2317
  as: buildAs,
1529
2318
  between: buildBetween,
2319
+ flag: buildFlag,
2320
+ pick: buildPick,
2321
+ yields: buildYields,
1530
2322
  prefix: buildPrefix,
1531
2323
  postfix: buildPostfix,
1532
2324
  manySep: (arg) => planSeparated(arg, false),
@@ -1546,7 +2338,7 @@ function createPlanner(config) {
1546
2338
  }
1547
2339
  function planSeq(children) {
1548
2340
  if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers");
1549
- const idiom = recognizeSeqIdiom(children);
2341
+ const idiom = idioms ? recognizeSeqIdiom(children) : null;
1550
2342
  if (idiom !== null) return planUncached(idiom);
1551
2343
  const PARSER = 0;
1552
2344
  const LABELLED = 1;
@@ -1556,6 +2348,7 @@ function createPlanner(config) {
1556
2348
  const kinds = [];
1557
2349
  const names = [];
1558
2350
  const tokens = [];
2351
+ const tokenIdList = [];
1559
2352
  const fns = [];
1560
2353
  const actionLogic = [];
1561
2354
  let hasAction = false;
@@ -1576,11 +2369,13 @@ function createPlanner(config) {
1576
2369
  kinds.push(LABELLED_TERMINAL);
1577
2370
  names.push(name);
1578
2371
  tokens.push(terminal2);
2372
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal2) : void 0);
1579
2373
  fns.push(null);
1580
2374
  } else {
1581
2375
  kinds.push(LABELLED);
1582
2376
  names.push(name);
1583
2377
  tokens.push(null);
2378
+ tokenIdList.push(void 0);
1584
2379
  fns.push(plan(inner));
1585
2380
  }
1586
2381
  continue;
@@ -1589,6 +2384,7 @@ function createPlanner(config) {
1589
2384
  kinds.push(ACTION);
1590
2385
  names.push(null);
1591
2386
  tokens.push(null);
2387
+ tokenIdList.push(void 0);
1592
2388
  fns.push(planAction(child.action));
1593
2389
  actionLogic.push(child.action);
1594
2390
  hasAction = true;
@@ -1599,11 +2395,13 @@ function createPlanner(config) {
1599
2395
  kinds.push(TERMINAL);
1600
2396
  names.push(null);
1601
2397
  tokens.push(terminal);
2398
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal) : void 0);
1602
2399
  fns.push(null);
1603
2400
  } else {
1604
2401
  kinds.push(PARSER);
1605
2402
  names.push(null);
1606
2403
  tokens.push(null);
2404
+ tokenIdList.push(void 0);
1607
2405
  fns.push(plan(child));
1608
2406
  }
1609
2407
  }
@@ -1624,7 +2422,7 @@ function createPlanner(config) {
1624
2422
  let value;
1625
2423
  if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
1626
2424
  const token = state.tokens[state.idx];
1627
- if (token !== void 0 && token.type === tokens[i]) {
2425
+ if (token !== void 0 && (token.id === tokenIdList[i] || token.id === void 0 && token.type === tokens[i])) {
1628
2426
  state.idx++;
1629
2427
  value = token;
1630
2428
  } else {
@@ -1654,18 +2452,46 @@ function createPlanner(config) {
1654
2452
  }
1655
2453
  function planAlt(branches) {
1656
2454
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches");
2455
+ const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null;
2456
+ if (oneOf !== null) return planUncached(oneOf);
2457
+ const factored = idioms ? recognizeAltPrefixIdiom(branches) : null;
2458
+ if (factored !== null) return planUncached(factored);
1657
2459
  const planned = branches.map(plan);
1658
2460
  const table = firsts ? buildDispatch(branches, firsts) : null;
1659
2461
  if (table !== null) {
1660
2462
  const byToken = /* @__PURE__ */ Object.create(null);
1661
- for (const [token, branch] of table) byToken[token] = planned[branches.indexOf(branch)];
2463
+ const byId = tokenIds ? [] : null;
2464
+ const groupClosures = /* @__PURE__ */ new Map();
2465
+ for (const [token, branch] of table) {
2466
+ let closure = groupClosures.get(branch);
2467
+ if (closure === void 0) {
2468
+ closure = Array.isArray(branch) ? (() => {
2469
+ const group = branch.map((b) => planned[branches.indexOf(b)]);
2470
+ return (state) => {
2471
+ const start = state.idx;
2472
+ for (let i = 0; i < group.length; i++) {
2473
+ const value = group[i](state);
2474
+ if (value !== FAIL) return value;
2475
+ state.idx = start;
2476
+ }
2477
+ return FAIL;
2478
+ };
2479
+ })() : planned[branches.indexOf(branch)];
2480
+ groupClosures.set(branch, closure);
2481
+ }
2482
+ byToken[token] = closure;
2483
+ if (byId) {
2484
+ const id = tokenIds.get(token);
2485
+ if (id !== void 0) byId[id] = closure;
2486
+ }
2487
+ }
1662
2488
  const expectedTokens = [...table.keys()];
1663
2489
  const expectedCount = expectedTokens.length;
1664
2490
  return (state) => {
1665
2491
  tick(state);
1666
2492
  const token = state.tokens[state.idx];
1667
2493
  if (token !== void 0) {
1668
- const branch = byToken[token.type];
2494
+ const branch = byId && token.id !== void 0 ? byId[token.id] : byToken[token.type];
1669
2495
  if (branch !== void 0) return branch(state);
1670
2496
  }
1671
2497
  for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i]);
@@ -1750,6 +2576,9 @@ var OPERATORS = [
1750
2576
  "prefix",
1751
2577
  "postfix",
1752
2578
  "between",
2579
+ "pick",
2580
+ "yields",
2581
+ "flag",
1753
2582
  "as",
1754
2583
  "subrule",
1755
2584
  "label",
@@ -1804,7 +2633,12 @@ var createValueMethods = () => ({
1804
2633
 
1805
2634
  // src/codegen.js
1806
2635
  function createCodegen(config) {
1807
- const { planner, rules, firsts, memo, maxSteps } = config;
2636
+ const { planner, rules, firsts, memo, maxSteps, tokenIds = null, memoSlots = /* @__PURE__ */ new Map(), inlineRules = true, idioms = true } = config;
2637
+ const typeCheck = (tokenVar, name) => {
2638
+ const id = tokenIds ? tokenIds.get(name) : void 0;
2639
+ if (id === void 0) return `${tokenVar}.type === ${JSON.stringify(name)}`;
2640
+ return `(${tokenVar}.id === ${id} || (${tokenVar}.id === undefined && ${tokenVar}.type === ${JSON.stringify(name)}))`;
2641
+ };
1808
2642
  const HELPERS = [];
1809
2643
  const ORIGINS = [];
1810
2644
  const RULES = [];
@@ -1820,6 +2654,33 @@ function createCodegen(config) {
1820
2654
  }
1821
2655
  const actionHelper = (logic) => helper(planner.planAction(logic), { kind: "action", logic });
1822
2656
  const constantHelper = (node) => helper(planner.plan(node), { kind: "constant", node });
2657
+ const MAX_INLINE_DEPTH = 6;
2658
+ const ONE_OF_SWITCH_AT = 12;
2659
+ const INLINE_OPS = /* @__PURE__ */ new Set(["infixLeft", "infixRight", "prefix", "postfix"]);
2660
+ const callSites = /* @__PURE__ */ new Map();
2661
+ (function count(node) {
2662
+ if (Array.isArray(node)) {
2663
+ for (const child of node) count(child);
2664
+ return;
2665
+ }
2666
+ if (!node || typeof node !== "object") return;
2667
+ if ("subrule" in node) {
2668
+ const target = Array.isArray(node.subrule) ? node.subrule[0] : node.subrule;
2669
+ if (typeof target === "string") callSites.set(target, (callSites.get(target) || 0) + 1);
2670
+ }
2671
+ for (const key of Object.keys(node)) count(node[key]);
2672
+ })(rules);
2673
+ const inlining = /* @__PURE__ */ new Set();
2674
+ let coldDepth = 0;
2675
+ const cold = (write) => {
2676
+ coldDepth++;
2677
+ try {
2678
+ return write();
2679
+ } finally {
2680
+ coldDepth--;
2681
+ }
2682
+ };
2683
+ 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]));
1823
2684
  function indexOfRule(name) {
1824
2685
  let index = ruleIndex.get(name);
1825
2686
  if (index !== void 0) return index;
@@ -1846,7 +2707,7 @@ function createCodegen(config) {
1846
2707
  out.push(
1847
2708
  `let ${result};`,
1848
2709
  `const ${token} = c.tokens[c.idx];`,
1849
- `if (${token} !== undefined && ${token}.type === ${JSON.stringify(name)}) { c.idx++; ${result} = ${token}; }`,
2710
+ `if (${token} !== undefined && ${typeCheck(token, name)}) { c.idx++; ${result} = ${token}; }`,
1850
2711
  `else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
1851
2712
  );
1852
2713
  return result;
@@ -1880,6 +2741,16 @@ function createCodegen(config) {
1880
2741
  );
1881
2742
  return result;
1882
2743
  }
2744
+ case "flag": {
2745
+ const start = nextId();
2746
+ out.push(`let ${result};`, `const ${start} = c.idx;`, "{");
2747
+ const value = gen(arg, out, bindingsVar);
2748
+ out.push(
2749
+ `if (${value} === F) { c.idx = ${start}; ${result} = false; } else ${result} = true;`,
2750
+ "}"
2751
+ );
2752
+ return result;
2753
+ }
1883
2754
  case "many":
1884
2755
  case "many1": {
1885
2756
  const atLeastOne = op === "many1";
@@ -1897,7 +2768,7 @@ function createCodegen(config) {
1897
2768
  );
1898
2769
  }
1899
2770
  out.push("for (;;) {", `const ${before} = c.idx;`);
1900
- const value = gen(arg, out, bindingsVar);
2771
+ const value = atLeastOne ? cold(() => gen(arg, out, bindingsVar)) : gen(arg, out, bindingsVar);
1901
2772
  out.push(
1902
2773
  `if (${value} === F) { c.idx = ${before}; break; }`,
1903
2774
  `if (c.idx === ${before}) P(${JSON.stringify(op)}, ${before});`,
@@ -1927,7 +2798,7 @@ function createCodegen(config) {
1927
2798
  operatorVar = gen(arg.operator, out, bindingsVar);
1928
2799
  out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`);
1929
2800
  }
1930
- const right = gen(arg.operand, out, bindingsVar);
2801
+ const right = cold(() => gen(arg.operand, out, bindingsVar));
1931
2802
  out.push(
1932
2803
  `if (${right} === F) { c.idx = ${mark}; break; }`,
1933
2804
  `if (${bindings} === null) ${bindings} = { left: null, op: null, right: null };`,
@@ -1942,12 +2813,27 @@ function createCodegen(config) {
1942
2813
  case "oneOf": {
1943
2814
  const names = Array.isArray(arg) ? arg : [arg];
1944
2815
  const token = nextId();
1945
- const test = names.map((n) => `${token}.type === ${JSON.stringify(n)}`).join(" || ");
2816
+ const ids = tokenIds ? names.map((n) => tokenIds.get(n)) : [];
2817
+ const failed = `else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(" ")} ${result} = F; }`;
2818
+ if (names.length > ONE_OF_SWITCH_AT && ids.every((id) => id !== void 0)) {
2819
+ const hit = nextId();
2820
+ const cases = [...new Set(ids)].map((id) => `case ${id}:`).join(" ");
2821
+ out.push(
2822
+ `let ${result};`,
2823
+ `const ${token} = c.tokens[c.idx];`,
2824
+ `let ${hit} = false;`,
2825
+ `if (${token} !== undefined) { switch (${token}.id !== undefined ? ${token}.id : I[${token}.type]) { ${cases} ${hit} = true; } }`,
2826
+ `if (${hit}) { c.idx++; ${result} = ${token}; }`,
2827
+ failed
2828
+ );
2829
+ return result;
2830
+ }
2831
+ const test = names.map((n) => typeCheck(token, n)).join(" || ");
1946
2832
  out.push(
1947
2833
  `let ${result};`,
1948
2834
  `const ${token} = c.tokens[c.idx];`,
1949
2835
  `if (${token} !== undefined && (${test})) { c.idx++; ${result} = ${token}; }`,
1950
- `else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(" ")} ${result} = F; }`
2836
+ failed
1951
2837
  );
1952
2838
  return result;
1953
2839
  }
@@ -1980,7 +2866,7 @@ function createCodegen(config) {
1980
2866
  operatorVar = gen(arg.operator, out, bindingsVar);
1981
2867
  out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`);
1982
2868
  }
1983
- const right = gen(arg.operand, out, bindingsVar);
2869
+ const right = cold(() => gen(arg.operand, out, bindingsVar));
1984
2870
  out.push(
1985
2871
  `if (${right} === F) { c.idx = ${mark}; break; }`,
1986
2872
  `if (${items} === null) ${items} = [];`,
@@ -2055,6 +2941,19 @@ function createCodegen(config) {
2055
2941
  );
2056
2942
  return result;
2057
2943
  }
2944
+ case "pick":
2945
+ case "yields": {
2946
+ const label2 = "L" + nextId();
2947
+ const start = nextId();
2948
+ out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
2949
+ const values = arg.parsers.map((parser) => {
2950
+ const value = gen(parser, out, bindingsVar);
2951
+ out.push(`if (${value} === F) { c.idx = ${start}; ${result} = F; break ${label2}; }`);
2952
+ return value;
2953
+ });
2954
+ out.push(`${result} = ${op === "pick" ? values[arg.index] : JSON.stringify(arg.value)};`, "}");
2955
+ return result;
2956
+ }
2058
2957
  case "prefix": {
2059
2958
  const label2 = "L" + nextId();
2060
2959
  const start = nextId();
@@ -2131,7 +3030,7 @@ function createCodegen(config) {
2131
3030
  );
2132
3031
  const sep = gen(sepNode, out, bindingsVar);
2133
3032
  out.push(`if (${sep} === F) { c.idx = ${beforeSep}; break; }`, `const ${beforeItem} = c.idx;`);
2134
- const item = gen(arg.rule, out, bindingsVar);
3033
+ const item = cold(() => gen(arg.rule, out, bindingsVar));
2135
3034
  out.push(
2136
3035
  `if (${item} === F) { c.idx = ${trailing ? beforeItem : beforeSep}; break; }`,
2137
3036
  `${result}.push(${item});`,
@@ -2146,8 +3045,20 @@ function createCodegen(config) {
2146
3045
  out.push(`const ${result} = ${helper(planner.plan(node), { kind: "unknownRule", name })}(c);`);
2147
3046
  return result;
2148
3047
  }
2149
- const index = indexOfRule(name);
2150
3048
  const parent = nextId();
3049
+ if (canInline(name)) {
3050
+ out.push(tickSrc);
3051
+ out.push(
3052
+ `const ${parent} = c.ruleDepth;`,
3053
+ `if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`
3054
+ );
3055
+ inlining.add(name);
3056
+ const inner = gen(rules[name], out, null);
3057
+ inlining.delete(name);
3058
+ out.push(`if (c.track) c.ruleDepth = ${parent};`);
3059
+ return inner;
3060
+ }
3061
+ const index = indexOfRule(name);
2151
3062
  out.push(tickSrc);
2152
3063
  if (memo) {
2153
3064
  const key = nextId();
@@ -2168,6 +3079,26 @@ function createCodegen(config) {
2168
3079
  `c.memo.set(${key}, { value: ${result}, end: c.idx });`,
2169
3080
  "}"
2170
3081
  );
3082
+ } else if (memoSlots.has(name)) {
3083
+ const table = nextId();
3084
+ const at = nextId();
3085
+ const end = nextId();
3086
+ out.push(
3087
+ `let ${result};`,
3088
+ `const ${table} = c.rm[${memoSlots.get(name)}];`,
3089
+ `const ${at} = c.idx;`,
3090
+ `const ${end} = ${table}.ends[${at}];`,
3091
+ `if (${end} !== 0) { c.idx = ${end} - 1; ${result} = ${table}.vals[${at}]; }`,
3092
+ "else {",
3093
+ `const ${parent} = c.ruleDepth;`,
3094
+ `if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
3095
+ `${result} = R[${index}](c);`,
3096
+ `if (c.track) c.ruleDepth = ${parent};`,
3097
+ `if (${result} === F) c.idx = ${at};`,
3098
+ `${table}.ends[${at}] = c.idx + 1;`,
3099
+ `${table}.vals[${at}] = ${result};`,
3100
+ "}"
3101
+ );
2171
3102
  } else {
2172
3103
  out.push(
2173
3104
  `const ${parent} = c.ruleDepth;`,
@@ -2185,7 +3116,7 @@ function createCodegen(config) {
2185
3116
  }
2186
3117
  function genSeq(children, out, result) {
2187
3118
  if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers");
2188
- const idiom = recognizeSeqIdiom(children);
3119
+ const idiom = idioms ? recognizeSeqIdiom(children) : null;
2189
3120
  if (idiom !== null) return gen(idiom, out, null);
2190
3121
  let hasLabels = false;
2191
3122
  let hasAction = false;
@@ -2221,24 +3152,53 @@ function createCodegen(config) {
2221
3152
  out.push(`${result} = ${hasAction ? actionResult : "[" + collected.join(", ") + "]"};`, "}");
2222
3153
  return result;
2223
3154
  }
3155
+ function genOrderedBacktrack(list, out, bindingsVar) {
3156
+ const result = nextId();
3157
+ const label2 = "L" + nextId();
3158
+ const start = nextId();
3159
+ out.push(`let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
3160
+ for (const branch of list) {
3161
+ out.push("{");
3162
+ const value = gen(branch, out, bindingsVar);
3163
+ out.push(`if (${value} !== F) { ${result} = ${value}; break ${label2}; }`, `c.idx = ${start};`, "}");
3164
+ }
3165
+ out.push(`${result} = F;`, "}");
3166
+ return result;
3167
+ }
2224
3168
  function genAlt(branches, out, result, bindingsVar) {
2225
3169
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches");
3170
+ const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null;
3171
+ if (oneOf !== null) return gen(oneOf, out, bindingsVar);
3172
+ const factored = idioms ? recognizeAltPrefixIdiom(branches) : null;
3173
+ if (factored !== null) return gen(factored, out, bindingsVar);
2226
3174
  const table = firsts ? buildDispatch(branches, firsts) : null;
2227
3175
  const label2 = "L" + nextId();
2228
3176
  if (table !== null) {
2229
3177
  const expected = [...table.keys()];
3178
+ const expectedIds = tokenIds ? expected.map((t) => tokenIds.get(t)) : null;
3179
+ const numeric = expectedIds !== null && expectedIds.every((id) => id !== void 0);
2230
3180
  const token = nextId();
2231
3181
  out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${token} = c.tokens[c.idx];`);
2232
3182
  out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(" ")} ${result} = F; break ${label2}; }`);
2233
- out.push(`switch (${token}.type) {`);
3183
+ let discriminant;
3184
+ if (numeric) {
3185
+ discriminant = nextId();
3186
+ out.push(`const ${discriminant} = ${token}.id !== undefined ? ${token}.id : I[${token}.type];`);
3187
+ out.push(`switch (${discriminant}) {`);
3188
+ } else {
3189
+ discriminant = `${token}.type`;
3190
+ out.push(`switch (${discriminant}) {`);
3191
+ }
2234
3192
  const byBranch = /* @__PURE__ */ new Map();
2235
- for (const [tokenName, branch] of table) {
3193
+ for (const tokenName of expected) {
3194
+ const branch = table.get(tokenName);
2236
3195
  if (!byBranch.has(branch)) byBranch.set(branch, []);
2237
3196
  byBranch.get(branch).push(tokenName);
2238
3197
  }
3198
+ const caseLabel = (t) => numeric ? tokenIds.get(t) : JSON.stringify(t);
2239
3199
  for (const [branch, tokenNames] of byBranch) {
2240
- out.push(tokenNames.map((t) => `case ${JSON.stringify(t)}:`).join(" ") + " {");
2241
- const value = gen(branch, out, bindingsVar);
3200
+ out.push(tokenNames.map((t) => `case ${caseLabel(t)}:`).join(" ") + " {");
3201
+ const value = Array.isArray(branch) ? genOrderedBacktrack(branch, out, bindingsVar) : gen(branch, out, bindingsVar);
2242
3202
  out.push(`${result} = ${value}; break ${label2};`, "}");
2243
3203
  }
2244
3204
  out.push(
@@ -2248,16 +3208,10 @@ function createCodegen(config) {
2248
3208
  );
2249
3209
  return result;
2250
3210
  }
2251
- const start = nextId();
2252
- out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
2253
- for (const branch of branches) {
2254
- out.push("{");
2255
- const value = gen(branch, out, bindingsVar);
2256
- out.push(`if (${value} !== F) { ${result} = ${value}; break ${label2}; }`, `c.idx = ${start};`, "}");
2257
- }
2258
- out.push(`${result} = F;`, "}");
2259
- return result;
3211
+ out.push(tickSrc);
3212
+ return genOrderedBacktrack(branches, out, bindingsVar);
2260
3213
  }
3214
+ const idByName = tokenIds ? Object.fromEntries(tokenIds) : null;
2261
3215
  function buildRule(name) {
2262
3216
  const out = [];
2263
3217
  const value = gen(rules[name], out, null);
@@ -2267,8 +3221,8 @@ return ${value};
2267
3221
  }`;
2268
3222
  SOURCES[ruleIndex.get(name)] = body;
2269
3223
  const source = `return ${body}`;
2270
- const factory = new Function("F", "E", "P", "T", "R", "H", source);
2271
- return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS);
3224
+ const factory = new Function("F", "E", "P", "T", "R", "H", "I", source);
3225
+ return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS, idByName);
2272
3226
  }
2273
3227
  function progressError(op, at) {
2274
3228
  throw new Error(
@@ -2309,6 +3263,165 @@ return ${value};
2309
3263
  return { compile, functionFor, sourceFor, ruleIndex, RULES, SOURCES, ORIGINS, guard };
2310
3264
  }
2311
3265
 
3266
+ // src/memoplan.js
3267
+ function selectMemoRules(grammar, firsts, options = {}) {
3268
+ const ll1 = options.ll1 !== false;
3269
+ const names = Object.keys(grammar);
3270
+ const leftEdges = /* @__PURE__ */ new Map();
3271
+ const callEdges = /* @__PURE__ */ new Map();
3272
+ for (const name of names) {
3273
+ const left = /* @__PURE__ */ new Set();
3274
+ leftmostCalls(grammar[name], firsts, left);
3275
+ leftEdges.set(name, [...left].filter((r) => r in grammar));
3276
+ const calls = /* @__PURE__ */ new Set();
3277
+ walk(grammar[name], (node) => {
3278
+ if (opOf(node) !== "subrule") return;
3279
+ const arg = node.subrule;
3280
+ const target = Array.isArray(arg) ? arg[0] : arg;
3281
+ if (typeof target === "string" && target in grammar) calls.add(target);
3282
+ });
3283
+ callEdges.set(name, [...calls]);
3284
+ }
3285
+ const closure = (edges, from) => {
3286
+ const seen = /* @__PURE__ */ new Set();
3287
+ const stack = [...from];
3288
+ while (stack.length > 0) {
3289
+ const next = stack.pop();
3290
+ if (seen.has(next)) continue;
3291
+ seen.add(next);
3292
+ for (const edge of edges.get(next) || []) stack.push(edge);
3293
+ }
3294
+ return seen;
3295
+ };
3296
+ const leftCache = /* @__PURE__ */ new Map();
3297
+ const leftReach = (name) => {
3298
+ let reach = leftCache.get(name);
3299
+ if (reach === void 0) {
3300
+ reach = closure(leftEdges, leftEdges.get(name) || []);
3301
+ leftCache.set(name, reach);
3302
+ }
3303
+ return reach;
3304
+ };
3305
+ const startsWith = (branch) => {
3306
+ const direct = /* @__PURE__ */ new Set();
3307
+ leftmostCalls(branch, firsts, direct);
3308
+ const all = /* @__PURE__ */ new Set();
3309
+ for (const rule of direct) {
3310
+ if (!(rule in grammar)) continue;
3311
+ all.add(rule);
3312
+ for (const deeper of leftReach(rule)) all.add(deeper);
3313
+ }
3314
+ return all;
3315
+ };
3316
+ const sites = [];
3317
+ for (const name of names) {
3318
+ walk(grammar[name], (node) => {
3319
+ if (opOf(node) !== "alt" || !Array.isArray(node.alt) || node.alt.length < 2) return;
3320
+ for (const group of collisionGroups(node.alt, firsts, ll1)) {
3321
+ const counts = /* @__PURE__ */ new Map();
3322
+ for (const branch of group) {
3323
+ for (const rule of startsWith(branch)) counts.set(rule, (counts.get(rule) || 0) + 1);
3324
+ }
3325
+ const shared = [...counts].filter(([, n]) => n >= 2).map(([rule]) => rule);
3326
+ const topmost = shared.filter((rule) => !shared.some((other) => other !== rule && leftReach(other).has(rule) && !leftReach(rule).has(other)));
3327
+ if (topmost.length > 0) sites.push({ host: name, rules: topmost });
3328
+ }
3329
+ });
3330
+ }
3331
+ const hosts = new Set(sites.map((site) => site.host));
3332
+ const chosen = /* @__PURE__ */ new Set();
3333
+ for (const { rules } of sites) {
3334
+ for (const rule of rules) {
3335
+ const inside = closure(callEdges, [rule]);
3336
+ inside.add(rule);
3337
+ for (const host of hosts) {
3338
+ if (inside.has(host)) {
3339
+ chosen.add(rule);
3340
+ break;
3341
+ }
3342
+ }
3343
+ }
3344
+ }
3345
+ const dependsOnCaller = (name) => {
3346
+ const reachable = closure(callEdges, [name]);
3347
+ reachable.add(name);
3348
+ for (const rule of reachable) if (usesOuterScope(grammar[rule])) return true;
3349
+ return false;
3350
+ };
3351
+ return names.filter((name) => chosen.has(name) && !dependsOnCaller(name));
3352
+ }
3353
+ function collisionGroups(branches, firsts, ll1) {
3354
+ const table = ll1 ? buildDispatch(branches, firsts) : null;
3355
+ if (table === null) return [branches];
3356
+ const groups = /* @__PURE__ */ new Set();
3357
+ for (const entry of table.values()) if (Array.isArray(entry) && entry.length > 1) groups.add(entry);
3358
+ return [...groups];
3359
+ }
3360
+ function walk(node, visit) {
3361
+ if (Array.isArray(node)) {
3362
+ for (const child of node) walk(child, visit);
3363
+ return;
3364
+ }
3365
+ const op = opOf(node);
3366
+ if (op === null) return;
3367
+ visit(node);
3368
+ const arg = node[op];
3369
+ switch (op) {
3370
+ case "seq":
3371
+ case "alt":
3372
+ if (Array.isArray(arg)) for (const child of arg) walk(child, visit);
3373
+ break;
3374
+ case "label":
3375
+ if (Array.isArray(arg)) walk(arg[1], visit);
3376
+ break;
3377
+ case "many":
3378
+ case "many1":
3379
+ case "option":
3380
+ case "flag":
3381
+ case "text":
3382
+ case "lookahead":
3383
+ case "negLookahead":
3384
+ walk(arg, visit);
3385
+ break;
3386
+ case "as":
3387
+ walk(Array.isArray(arg) ? arg[0] : arg && arg.parser, visit);
3388
+ break;
3389
+ case "between":
3390
+ if (arg) for (const part of [arg.open, arg.body, arg.close]) walk(part, visit);
3391
+ break;
3392
+ case "pick":
3393
+ case "yields":
3394
+ if (arg && Array.isArray(arg.parsers)) for (const part of arg.parsers) walk(part, visit);
3395
+ break;
3396
+ case "prefix":
3397
+ if (arg) {
3398
+ walk(arg.operator, visit);
3399
+ walk(arg.operand, visit);
3400
+ }
3401
+ break;
3402
+ case "postfix":
3403
+ if (arg) {
3404
+ walk(arg.operand, visit);
3405
+ walk(arg.suffix, visit);
3406
+ }
3407
+ break;
3408
+ case "infixLeft":
3409
+ case "infixRight":
3410
+ if (arg) {
3411
+ walk(arg.operand, visit);
3412
+ if (arg.operator !== void 0) walk(arg.operator, visit);
3413
+ }
3414
+ break;
3415
+ case "manySep":
3416
+ case "many1Sep":
3417
+ if (arg) {
3418
+ walk(arg.rule, visit);
3419
+ if (typeof arg.sep !== "string") walk(arg.sep, visit);
3420
+ }
3421
+ break;
3422
+ }
3423
+ }
3424
+
2312
3425
  // src/parser.js
2313
3426
  var EXECUTION_MODES = /* @__PURE__ */ new Set(["generated", "interpreted"]);
2314
3427
  var GrammarError = class extends Error {
@@ -2324,6 +3437,9 @@ function createParser(spec, options = {}) {
2324
3437
  const { tokens: tokenDefs, rules, start } = spec;
2325
3438
  const {
2326
3439
  memo = false,
3440
+ autoMemo = true,
3441
+ inlineRules = true,
3442
+ idioms = true,
2327
3443
  strict = true,
2328
3444
  validate = true,
2329
3445
  maxSteps = 0,
@@ -2350,9 +3466,12 @@ function createParser(spec, options = {}) {
2350
3466
  for (const [name, method] of Object.entries(extraMethods || {})) engine.addMethod(name, method);
2351
3467
  const startRule = rules[start];
2352
3468
  const firsts = ll1 ? analysis.firsts : null;
2353
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval });
3469
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]));
3470
+ const memoRules = autoMemo && !memo && analysis.errors.length === 0 ? selectMemoRules(rules, analysis.firsts, { ll1 }) : [];
3471
+ const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]));
3472
+ const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds, memoSlots, idioms });
2354
3473
  engine.__jlGrammar = { planner };
2355
- const codegen = compile && unsafeEval ? createCodegen({ planner, rules, firsts, memo, maxSteps }) : null;
3474
+ const codegen = compile && unsafeEval ? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms }) : null;
2356
3475
  const compiledStart = codegen && codegen.compile(start);
2357
3476
  if (codegen && !engine.disableInterpretedOptimization) {
2358
3477
  for (const name of Object.keys(rules)) {
@@ -2361,7 +3480,7 @@ function createParser(spec, options = {}) {
2361
3480
  }
2362
3481
  }
2363
3482
  function parseTokens(tokens, source, track = false) {
2364
- const state = createState(tokens, rules, { memo, maxSteps, track });
3483
+ const state = createState(tokens, rules, { memo, maxSteps, track, memoSlots: memoRules.length });
2365
3484
  state.firsts = firsts;
2366
3485
  state.source = source;
2367
3486
  state.positions = lexer.positions;
@@ -2391,6 +3510,8 @@ function createParser(spec, options = {}) {
2391
3510
  planner,
2392
3511
  codegen,
2393
3512
  execution: mode,
3513
+ /** Rules memoized by `autoMemo`, for introspection. */
3514
+ memoRules,
2394
3515
  compiled: Boolean(compiledStart),
2395
3516
  /** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
2396
3517
  get firsts() {
@@ -2696,6 +3817,7 @@ var CONSTRUCTORS = {
2696
3817
  many: { arity: [1, 1], of: ([a]) => ({ many: a }) },
2697
3818
  many1: { arity: [1, 1], of: ([a]) => ({ many1: a }) },
2698
3819
  option: { arity: [1, 1], of: ([a]) => ({ option: a }) },
3820
+ flag: { arity: [1, 1], of: ([a]) => ({ flag: a }) },
2699
3821
  action: { arity: [1, 1], of: ([a]) => ({ action: a }) },
2700
3822
  lookahead: { arity: [1, 1], of: ([a]) => ({ lookahead: a }) },
2701
3823
  negLookahead: { arity: [1, 1], of: ([a]) => ({ negLookahead: a }) },
@@ -2707,6 +3829,10 @@ var CONSTRUCTORS = {
2707
3829
  manySep: { arity: [2, 2], of: ([a, b]) => ({ manySep: { rule: a, sep: b } }) },
2708
3830
  many1Sep: { arity: [2, 2], of: ([a, b]) => ({ many1Sep: { rule: a, sep: b } }) },
2709
3831
  between: { arity: [3, 3], of: ([a, b, c]) => ({ between: { open: a, body: b, close: c } }) },
3832
+ // pick(1, consume(Open), subrule(Body), consume(Close)) — the result at that index.
3833
+ pick: { arity: [2, Infinity], of: ([index, ...parsers]) => ({ pick: { parsers, index } }) },
3834
+ // yields(consume(True), true) — run the parsers, give back the constant.
3835
+ yields: { arity: [2, Infinity], of: (args) => ({ yields: { parsers: args.slice(0, -1), value: args[args.length - 1] } }) },
2710
3836
  infixLeft: { arity: [3, 3], of: ([a, b, c]) => ({ infixLeft: { operand: a, operator: b, combine: logicOf(c) } }) },
2711
3837
  infixRight: { arity: [3, 3], of: ([a, b, c]) => ({ infixRight: { operand: a, operator: b, combine: logicOf(c) } }) },
2712
3838
  prefix: { arity: [3, 3], of: ([a, b, c]) => ({ prefix: { operator: a, operand: b, combine: logicOf(c) } }) },
@@ -2727,6 +3853,355 @@ function createParserFromSource(source, options = {}) {
2727
3853
  return createParser(grammar, { ...options, methods });
2728
3854
  }
2729
3855
 
3856
+ // src/lexgen.js
3857
+ function generateLexer(defs, options = {}) {
3858
+ const positions = options.positions === "offset" ? "offset" : "full";
3859
+ const trackLines = positions === "full";
3860
+ const useScanners = options.scanners !== false;
3861
+ const useFuse = options.fuse !== false;
3862
+ const modeIds = /* @__PURE__ */ new Map([[DEFAULT_MODE2, 0]]);
3863
+ const modeNames = [DEFAULT_MODE2];
3864
+ const idOf = (name) => {
3865
+ let id = modeIds.get(name);
3866
+ if (id === void 0) {
3867
+ id = modeNames.length;
3868
+ modeIds.set(name, id);
3869
+ modeNames.push(name);
3870
+ }
3871
+ return id;
3872
+ };
3873
+ for (const def of defs) for (const mode of modesOf2(def)) idOf(mode);
3874
+ const compiled = defs.map((def, id) => {
3875
+ const flags = def.ignoreCase ? "iy" : "y";
3876
+ return {
3877
+ id,
3878
+ name: def.name,
3879
+ skip: !!def.skip,
3880
+ literal: def.literal,
3881
+ pattern: def.pattern,
3882
+ flags,
3883
+ longerAlt: def.longerAlt || null,
3884
+ scanner: useScanners && def.pattern ? buildScanner(def.pattern, !!def.ignoreCase) : null,
3885
+ modes: modesOf2(def),
3886
+ pushMode: def.pushMode ? idOf(def.pushMode) : -1,
3887
+ popMode: !!def.popMode,
3888
+ 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)
3889
+ };
3890
+ });
3891
+ const byName = new Map(compiled.map((c) => [c.name, c]));
3892
+ const multiMode = modeNames.length > 1 || compiled.some((c) => c.popMode || c.pushMode !== -1);
3893
+ const regexes = [];
3894
+ const regexIndex = /* @__PURE__ */ new Map();
3895
+ const rxFor = (c) => {
3896
+ if (!regexIndex.has(c.id)) {
3897
+ regexIndex.set(c.id, regexes.length);
3898
+ regexes.push([c.id, c.flags]);
3899
+ }
3900
+ return (
3901
+ /** @type {number} */
3902
+ regexIndex.get(c.id)
3903
+ );
3904
+ };
3905
+ const fusedRuns = [];
3906
+ const helpers = [];
3907
+ const keywords = [];
3908
+ const lookFns = /* @__PURE__ */ new Map();
3909
+ const need = { digits: false, hex: false, keyword: false };
3910
+ function emitRegex(c) {
3911
+ const rx = rxFor(c);
3912
+ if (c.skip) {
3913
+ 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 }`;
3914
+ }
3915
+ 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 }`;
3916
+ }
3917
+ const emitted = /* @__PURE__ */ new Map();
3918
+ function emitDef(c) {
3919
+ let code = emitted.get(c.id);
3920
+ if (code === void 0) {
3921
+ code = emitDefOnce(c);
3922
+ emitted.set(c.id, code);
3923
+ }
3924
+ return code;
3925
+ }
3926
+ function emitDefOnce(c) {
3927
+ if (c.literal !== void 0) {
3928
+ const lit = c.literal;
3929
+ const test = lit.length === 1 ? `code === ${lit.charCodeAt(0)}` : `text.startsWith(${q(lit)}, offset)`;
3930
+ return `if (${test}) { id = ${c.id}; end = offset + ${lit.length}; matched = ${q(lit)}; break found }`;
3931
+ }
3932
+ const s = c.scanner;
3933
+ if (s === null) return emitRegex(c);
3934
+ const hit = `{ id = ${c.id}; end = e; matched = null; break found }`;
3935
+ switch (s.kind) {
3936
+ case SCANNER_KINDS.CLASS: {
3937
+ if (s.wide) {
3938
+ helpers.push(wideClassHelper(c.id, s));
3939
+ return `{ const e = scan${c.id}(text, offset, length); if (e === -2) { ${emitRegex(c)} } else if (e !== -1) ${hit} }`;
3940
+ }
3941
+ 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 }`;
3942
+ }
3943
+ case SCANNER_KINDS.SEQ2:
3944
+ 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 } }`;
3945
+ case SCANNER_KINDS.KEYWORD: {
3946
+ need.keyword = true;
3947
+ const cond = condition(s.look, "c");
3948
+ let look = lookFns.get(cond);
3949
+ if (look === void 0) {
3950
+ look = `look${lookFns.size}`;
3951
+ lookFns.set(cond, look);
3952
+ }
3953
+ keywords.push([s.lower, s.upper]);
3954
+ return `{ const e = scanKeyword(text, offset, length, KW[${keywords.length - 1}][0], KW[${keywords.length - 1}][1], ${look}); if (e !== -1) ${hit} }`;
3955
+ }
3956
+ case SCANNER_KINDS.LINE:
3957
+ 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 }`;
3958
+ case SCANNER_KINDS.QUOTED:
3959
+ helpers.push(quotedHelper(c.id, s));
3960
+ return `{ const e = scan${c.id}(text, offset); if (e !== -1) ${hit} }`;
3961
+ case SCANNER_KINDS.NUMBER:
3962
+ need.digits = true;
3963
+ helpers.push(numberHelper(c.id, s));
3964
+ return `{ const e = scan${c.id}(text, offset, length); if (e !== -1) ${hit} }`;
3965
+ case SCANNER_KINDS.STRING:
3966
+ if (s.uni) need.hex = true;
3967
+ helpers.push(stringHelper(c.id, s));
3968
+ return `{ const e = scan${c.id}(text, offset, length); if (e !== -1) ${hit} }`;
3969
+ default:
3970
+ return emitRegex(c);
3971
+ }
3972
+ }
3973
+ const fusible = (c) => c.literal === void 0 && c.scanner === null && !/\\[1-9]|\\k</.test(c.pattern) && new RegExp(c.pattern + "|").exec("").length === 1;
3974
+ function emitList(list) {
3975
+ const out = [];
3976
+ for (let i = 0; i < list.length; ) {
3977
+ let j = i;
3978
+ while (useFuse && j < list.length && fusible(list[j]) && list[j].flags === list[i].flags) j++;
3979
+ if (j - i < 3) {
3980
+ out.push(emitDef(list[i]));
3981
+ i++;
3982
+ continue;
3983
+ }
3984
+ const run = list.slice(i, j);
3985
+ run.forEach(rxFor);
3986
+ const k = fusedRuns.length;
3987
+ fusedRuns.push({ flags: run[0].flags, ids: run.map((d) => d.id), rx: run.map((d) => (
3988
+ /** @type {number} */
3989
+ regexIndex.get(d.id)
3990
+ )) });
3991
+ out.push(
3992
+ `FX[${k}].lastIndex = offset; m = FX[${k}].exec(text)
3993
+ if (m !== null) {
3994
+ 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 }
3995
+ // An empty match is "no match" to a lexer, so the members after it still get their turn.
3996
+ 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 } }
3997
+ }`
3998
+ );
3999
+ i = j;
4000
+ }
4001
+ return out.join("\n ");
4002
+ }
4003
+ const lists = [{ list: (
4004
+ /** @type {any[]} */
4005
+ []
4006
+ ) }];
4007
+ const listIds = /* @__PURE__ */ new Map([["", 0]]);
4008
+ const listFor = (list) => {
4009
+ const key = list.map((c) => c.id).join(",");
4010
+ let id = listIds.get(key);
4011
+ if (id === void 0) {
4012
+ id = lists.length;
4013
+ listIds.set(key, id);
4014
+ lists.push({ list });
4015
+ }
4016
+ return id;
4017
+ };
4018
+ const WIDTH = 129;
4019
+ const table = [];
4020
+ for (const name of modeNames) {
4021
+ const members = compiled.filter((c) => c.modes.includes(name));
4022
+ if (members.length === 0) throw new Error(`generateLexer: mode '${name}' has no tokens`);
4023
+ const dispatch = members.some((c) => c.firstChars !== null);
4024
+ for (let code = 0; code < 128; code++) {
4025
+ table.push(listFor(dispatch ? members.filter((c) => c.firstChars === null || c.firstChars.has(code)) : members));
4026
+ }
4027
+ table.push(listFor(dispatch ? members.filter((c) => c.firstChars === null || [...c.firstChars].some((x) => x >= 128)) : members));
4028
+ }
4029
+ if (lists.length > 6e4) throw new Error("generateLexer: too many distinct candidate lists");
4030
+ const dispatchTable = String.fromCharCode(...table.map((id) => 256 + id));
4031
+ const listCases = lists.slice(1).map(({ list }, i) => `case ${i + 1}: {
4032
+ ${emitList(list)}
4033
+ break
4034
+ }`);
4035
+ const longer = compiled.filter((c) => c.longerAlt);
4036
+ const longerCode = longer.length === 0 ? "" : `
4037
+ switch (id) {
4038
+ ${longer.map((c) => {
4039
+ const alt2 = (
4040
+ /** @type {any} */
4041
+ byName.get(
4042
+ /** @type {string} */
4043
+ c.longerAlt
4044
+ )
4045
+ );
4046
+ 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)`;
4047
+ 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 }`;
4048
+ }).join("\n")}
4049
+ }`;
4050
+ const transitions = compiled.filter((c) => c.popMode || c.pushMode !== -1);
4051
+ const describe = trackLines ? "'line ' + line + ' column ' + (offset - lineStart + 1)" : "'offset ' + offset";
4052
+ const transitionCode = transitions.length === 0 ? "" : `
4053
+ switch (id) {
4054
+ ${transitions.map((c) => c.popMode ? ` case ${c.id}:
4055
+ 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"} })
4056
+ mode = stack[--depth]
4057
+ break` : ` case ${c.id}: stack[depth++] = mode; mode = ${c.pushMode}; break`).join("\n")}
4058
+ }`;
4059
+ const where = trackLines ? "'line ' + line + ' column ' + column" : "'offset ' + offset";
4060
+ const inMode = modeNames.length > 1 ? " + ' (in lexer mode \\'' + MODE_NAMES[mode] + '\\')'" : "";
4061
+ return `(() => {
4062
+ const NAMES = tokens.map((t) => t.name)
4063
+ const SKIP = tokens.map((t) => (t.skip ? 1 : 0))
4064
+ const DISPATCH = ${q(dispatchTable)}
4065
+ ${modeNames.length > 1 ? `const MODE_NAMES = ${q(modeNames)}` : ""}
4066
+ // Patterns are read from the token table above rather than repeated here.
4067
+ const RX = ${q(regexes)}.map(([i, flags]) => new RegExp(tokens[i].pattern, flags))
4068
+ ${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))
4069
+ const FIDS = ${q(fusedRuns.map((f) => f.ids))}
4070
+ const FRX = ${q(fusedRuns.map((f) => f.rx))}` : ""}
4071
+ ${need.digits ? "const skipDigits = (text, i, length) => { while (i < length) { const c = text.charCodeAt(i); if (c < 48 || c > 57) break; i++ } return i }" : ""}
4072
+ ${need.hex ? "const isHex = (c) => (c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102)" : ""}
4073
+ ${need.keyword ? `const KW = ${q(keywords)}
4074
+ ${[...lookFns].map(([cond, name]) => `const ${name} = (c) => ${cond}`).join("\n ")}
4075
+ ${KEYWORD_HELPER}` : ""}
4076
+ ${helpers.join("\n")}
4077
+
4078
+ function tokenize (text) {
4079
+ if (typeof text !== 'string') throw new TypeError('tokenize: expected a string')
4080
+ const tokens_ = []
4081
+ let offset = 0
4082
+ let line = 1
4083
+ let lineStart = 0
4084
+ const length = text.length
4085
+ ${multiMode ? "let mode = 0\n const stack = []\n let depth = 0" : ""}
4086
+ let m = null
4087
+ while (offset < length) {
4088
+ const code = text.charCodeAt(offset)
4089
+ let id = -1
4090
+ let end = -1
4091
+ let matched = null
4092
+ found: {
4093
+ switch (DISPATCH.charCodeAt(${multiMode ? `mode * ${WIDTH} + ` : ""}(code < 128 ? code : 128)) - 256) {
4094
+ ${listCases.join("\n ")}
4095
+ }
4096
+ }
4097
+ if (id === -1) {
4098
+ const column = offset - lineStart + 1
4099
+ throw new LexError('Unexpected character \\'' + text[offset] + '\\' at ' + ${where}${inMode}, { offset, line: ${trackLines ? "line" : "0"}, col: ${trackLines ? "column" : "0"} })
4100
+ }${longerCode}
4101
+ if (SKIP[id] === 0) {
4102
+ const image = matched !== null ? matched : text.slice(offset, end)
4103
+ tokens_.push(${trackLines ? "{ type: NAMES[id], image, start: offset, end, line, col: offset - lineStart + 1 }" : "{ type: NAMES[id], id, image, start: offset, end }"})
4104
+ }
4105
+ ${trackLines ? "for (let j = offset; j < end; j++) { if (text.charCodeAt(j) !== 10) continue; line++; lineStart = j + 1 }" : ""}${transitionCode}
4106
+ offset = end
4107
+ }
4108
+ return tokens_
4109
+ }
4110
+ return { tokenize, tokenNames: NAMES, positions: ${q(positions)}, modeNames: ${q(modeNames)} }
4111
+ })()`;
4112
+ }
4113
+ var DEFAULT_MODE2 = "default";
4114
+ var q = JSON.stringify;
4115
+ function modesOf2(def) {
4116
+ if (Array.isArray(def.modes) && def.modes.length > 0) return def.modes;
4117
+ if (def.mode) return [def.mode];
4118
+ return [DEFAULT_MODE2];
4119
+ }
4120
+ function condition(table, v) {
4121
+ const parts = [];
4122
+ for (let i = 0; i < 128; ) {
4123
+ if (table[i] !== 1) {
4124
+ i++;
4125
+ continue;
4126
+ }
4127
+ let j = i;
4128
+ while (j + 1 < 128 && table[j + 1] === 1) j++;
4129
+ parts.push(i === j ? `${v} === ${i}` : `(${v} >= ${i} && ${v} <= ${j})`);
4130
+ i = j + 1;
4131
+ }
4132
+ return parts.length > 0 ? parts.join(" || ") : "false";
4133
+ }
4134
+ var KEYWORD_HELPER = `function scanKeyword (text, offset, length, lower, upper, look) {
4135
+ const n = lower.length
4136
+ if (offset + n > length) return -1
4137
+ for (let i = 0; i < n; i++) {
4138
+ const c = text.charCodeAt(offset + i)
4139
+ if (c !== lower[i] && c !== upper[i]) return -1
4140
+ }
4141
+ const end = offset + n
4142
+ if (end < length && look(text.charCodeAt(end))) return -1
4143
+ return end
4144
+ }`;
4145
+ function wideClassHelper(id, s) {
4146
+ return `function scan${id} (text, offset, length) {
4147
+ const code = text.charCodeAt(offset)
4148
+ if (code >= 128) return -2
4149
+ if (!(${condition(s.first, "code")})) return -1
4150
+ let j = offset + 1
4151
+ while (j < length) {
4152
+ const c = text.charCodeAt(j)
4153
+ if (c >= 128) return -2
4154
+ if (!(${condition(s.rest, "c")})) break
4155
+ j++
4156
+ }
4157
+ return j
4158
+ }`;
4159
+ }
4160
+ function quotedHelper(id, s) {
4161
+ return `function scan${id} (text, offset) {
4162
+ if (text.charCodeAt(offset) !== ${s.quote}) return -1
4163
+ let fallback = -1
4164
+ let j = offset + 1
4165
+ for (;;) {
4166
+ j = text.indexOf(${q(String.fromCharCode(s.quote))}, j)
4167
+ if (j === -1) return fallback
4168
+ if (text.charCodeAt(j + 1) !== ${s.quote}) return j + 1
4169
+ fallback = j + 1
4170
+ j += 2
4171
+ }
4172
+ }`;
4173
+ }
4174
+ function numberHelper(id, s) {
4175
+ return `function scan${id} (text, offset, length) {
4176
+ let j = offset
4177
+ ${s.sign ? "if (text.charCodeAt(j) === 45) j++" : ""}
4178
+ const c = text.charCodeAt(j)
4179
+ ${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)"}
4180
+ ${s.frac ? "if (text.charCodeAt(j) === 46) { const k = skipDigits(text, j + 1, length); if (k > j + 1) j = k }" : ""}
4181
+ ${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 }" : ""}
4182
+ return j
4183
+ }`;
4184
+ }
4185
+ function stringHelper(id, s) {
4186
+ return `function scan${id} (text, offset, length) {
4187
+ if (text.charCodeAt(offset) !== ${s.quote}) return -1
4188
+ let j = offset + 1
4189
+ for (;;) {
4190
+ if (j >= length) return -1
4191
+ const c = text.charCodeAt(j)
4192
+ if (c === ${s.quote}) return j + 1
4193
+ if (c === 92) {
4194
+ const d = text.charCodeAt(j + 1)
4195
+ ${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
4196
+ ${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`}
4197
+ continue
4198
+ }
4199
+ ${s.ctrl ? "if (c < 32) return -1" : ""}
4200
+ j++
4201
+ }
4202
+ }`;
4203
+ }
4204
+
2730
4205
  // src/emit.js
2731
4206
  var import_json_logic_engine2 = require("json-logic-engine");
2732
4207
  var UNSUPPORTED = /\b(coerceArray|precoerceNumber|compareCheck|assertAllowedDepth|assertSize|asyncIterators|notTraversed)\b|(?<!engine\.)\bmethods\[|engine\.(?!methods\b)/;
@@ -2775,11 +4250,21 @@ function emitModule(spec, options = {}) {
2775
4250
  const { tokens: tokenDefs, rules, start } = spec;
2776
4251
  const {
2777
4252
  memo = false,
4253
+ autoMemo = true,
4254
+ // Off here, on in `createParser`. Inlining a precedence chain is worth 2-3x on a bare chain but only
4255
+ // 1.02-1.04x on the formula and SQL examples, and it makes the file bigger: the formula module grows
4256
+ // 32% minified (38 to 50 KB) even after the second copy of each operand is kept as a call. A parser
4257
+ // built at run time pays nothing for that; a file someone ships does.
4258
+ inlineRules = false,
4259
+ idioms = true,
2778
4260
  strict = true,
2779
4261
  ll1 = true,
2780
4262
  maxSteps = 0,
2781
4263
  positions = "full",
2782
4264
  execution = "generated",
4265
+ lexer: lexerMode = "generated",
4266
+ scanners = true,
4267
+ fuse = true,
2783
4268
  format = "esm",
2784
4269
  validate = true,
2785
4270
  moduleName = spec.name || "parser",
@@ -2788,6 +4273,7 @@ function emitModule(spec, options = {}) {
2788
4273
  methods: semanticMethods
2789
4274
  } = options;
2790
4275
  if (format !== "esm" && format !== "cjs") throw new Error(`emitModule: format must be 'esm' or 'cjs', got ${quote(format)}`);
4276
+ if (lexerMode !== "generated" && lexerMode !== "runtime") throw new Error(`emitModule: lexer must be 'generated' or 'runtime', got ${quote(lexerMode)}`);
2791
4277
  const methodsBlock = options.methodsBlock !== void 0 ? options.methodsBlock : spec.methodsBlock;
2792
4278
  const block = methodsBlock ? emitMethodsBlock(methodsBlock, format) : null;
2793
4279
  if (!start || !(start in rules)) throw new Error(`emitModule: start rule '${start}' is not defined in rules`);
@@ -2795,12 +4281,15 @@ function emitModule(spec, options = {}) {
2795
4281
  const analysis = analyze(rules, { tokenNames: lexer.tokenNames });
2796
4282
  if (validate && analysis.errors.length > 0) throw new GrammarError(analysis.errors);
2797
4283
  const firsts = ll1 ? analysis.firsts : null;
4284
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]));
2798
4285
  const engine = { build: () => {
2799
4286
  throw new Error("not compiled during emit");
2800
4287
  }, run: () => {
2801
4288
  } };
2802
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false });
2803
- const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps });
4289
+ const memoRules = autoMemo && !memo && analysis.errors.length === 0 ? selectMemoRules(rules, analysis.firsts, { ll1 }) : [];
4290
+ const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]));
4291
+ const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false, tokenIds, memoSlots, idioms });
4292
+ const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms });
2804
4293
  codegen.compile(start);
2805
4294
  const reached = [...codegen.ruleIndex.entries()].sort((a, b) => a[1] - b[1]);
2806
4295
  const unsupported = codegen.ORIGINS.map((origin, index) => ({ origin, index })).filter(({ origin }) => origin.kind === "unknownRule");
@@ -2841,7 +4330,8 @@ R[${index}] = ${codegen.SOURCES[index]}`
2841
4330
  ).join("\n\n");
2842
4331
  const interpretedActions = execution === "interpreted";
2843
4332
  const cjs = format === "cjs";
2844
- const runtimeImports = ["FAIL as F", "expect as E", "createState", "buildParseError", "createLexer"];
4333
+ const generatedLexer = lexerMode === "generated";
4334
+ const runtimeImports = ["FAIL as F", "expect as E", "createState", "buildParseError", generatedLexer ? "LexError" : "createLexer"];
2845
4335
  if (needsEngine) runtimeImports.push("createValueMethods");
2846
4336
  const importRuntime = cjs ? `const { ${runtimeImports.map((b) => b.replace(" as ", ": ")).join(", ")} } = require(${quote(runtimeSpecifier)})` : `import { ${runtimeImports.join(", ")} } from ${quote(runtimeSpecifier)}`;
2847
4337
  const importEngine = engineFree ? "" : cjs ? `const { LogicEngine } = require(${quote(engineSpecifier)})
@@ -2860,7 +4350,7 @@ export default { ${PUBLIC.join(", ")} }`;
2860
4350
  //
2861
4351
  // The grammar was analyzed when this file was written: FIRST sets, left-recursion
2862
4352
  // checks and rule specialization all happened then. Importing it does no grammar
2863
- // work and generates no code.
4353
+ // work and generates no code.${generatedLexer ? "\n// The lexer was written out as source too, so there is no lexer engine to load." : ""}
2864
4354
  ${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."}
2865
4355
 
2866
4356
  ${importRuntime}
@@ -2870,7 +4360,7 @@ const tokens = ${literal(tokenDefs)}
2870
4360
 
2871
4361
  const start = ${quote(start)}
2872
4362
 
2873
- const lexer = createLexer(tokens, { positions: ${quote(positions)} })
4363
+ const lexer = ${generatedLexer ? generateLexer(tokenDefs, { positions, scanners, fuse }) : `createLexer(tokens, { positions: ${quote(positions)}, scanners: ${scanners}, fuse: ${fuse} })`}
2874
4364
  const tokenize = lexer.tokenize
2875
4365
 
2876
4366
  ${engineFree ? `// Every semantic action compiled to source, so the engine reduced to a table
@@ -2943,6 +4433,14 @@ const H = [
2943
4433
  ${helpers.map((source, index) => ` /* ${index} */ ${source}`).join(",\n")}
2944
4434
  ]
2945
4435
 
4436
+ /**
4437
+ * Name -> id, the same numbering \`lexer\` assigns. A generated \`alt\`'s switch
4438
+ * dispatches on a token's id when it has one; this is only read as a fallback,
4439
+ * for a hand-built token from \`parseTokens\` that bypassed this file's own
4440
+ * lexer and so has no id of its own.
4441
+ */
4442
+ const I = ${literal(Object.fromEntries(tokenIds))}
4443
+
2946
4444
  ${ruleSources}
2947
4445
 
2948
4446
  const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name, index])))}
@@ -2953,7 +4451,7 @@ const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name,
2953
4451
  * @param {string} [source]
2954
4452
  */
2955
4453
  function parseTokens (tokenList, source, track = false) {
2956
- const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track })
4454
+ const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track, memoSlots: ${memoRules.length} })
2957
4455
  state.source = source
2958
4456
  state.positions = ${quote(positions)}
2959
4457
  state.ruleNames[0] = start