parseman 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/analysis/gating.d.ts +115 -0
  2. package/dist/analysis/gating.d.ts.map +1 -0
  3. package/dist/combinators/first-set.d.ts +25 -0
  4. package/dist/combinators/first-set.d.ts.map +1 -1
  5. package/dist/combinators/gate.d.ts +24 -0
  6. package/dist/combinators/gate.d.ts.map +1 -0
  7. package/dist/combinators/grammar.d.ts.map +1 -1
  8. package/dist/combinators/guard.d.ts +5 -12
  9. package/dist/combinators/guard.d.ts.map +1 -1
  10. package/dist/combinators/parser.d.ts +7 -0
  11. package/dist/combinators/parser.d.ts.map +1 -1
  12. package/dist/combinators/regex.d.ts.map +1 -1
  13. package/dist/combinators/scanTo.d.ts +45 -3
  14. package/dist/combinators/scanTo.d.ts.map +1 -1
  15. package/dist/compiler/codegen.d.ts +78 -14
  16. package/dist/compiler/codegen.d.ts.map +1 -1
  17. package/dist/compiler/ir-serialize.d.ts +1 -1
  18. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  19. package/dist/compiler/linker.d.ts.map +1 -1
  20. package/dist/functional/run.d.ts.map +1 -1
  21. package/dist/index.cjs +760 -246
  22. package/dist/index.cjs.map +4 -4
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +760 -246
  26. package/dist/index.js.map +4 -4
  27. package/dist/language-service/index.cjs +2 -0
  28. package/dist/language-service/index.cjs.map +2 -2
  29. package/dist/language-service/index.js +2 -0
  30. package/dist/language-service/index.js.map +2 -2
  31. package/dist/plugin/index.cjs +803 -270
  32. package/dist/plugin/index.cjs.map +4 -4
  33. package/dist/plugin/index.d.ts.map +1 -1
  34. package/dist/plugin/index.js +803 -270
  35. package/dist/plugin/index.js.map +4 -4
  36. package/dist/spec/index.cjs.map +1 -1
  37. package/dist/spec/index.js.map +1 -1
  38. package/dist/types.d.ts +18 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/version.d.ts +17 -0
  41. package/dist/version.d.ts.map +1 -0
  42. package/package.json +2 -1
@@ -95,10 +95,13 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
95
95
  return true;
96
96
  }
97
97
  }
98
+ function isZeroWidthAssertion(p) {
99
+ return p._def.tag === "not";
100
+ }
98
101
  function sequenceFirstSet(parsers) {
99
102
  let fs2 = empty();
100
103
  for (const p of parsers) {
101
- fs2 = union(fs2, p._meta.firstSet);
104
+ if (!isZeroWidthAssertion(p)) fs2 = union(fs2, p._meta.firstSet);
102
105
  if (!matchesEmpty(p)) return fs2;
103
106
  }
104
107
  return fs2;
@@ -128,7 +131,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
128
131
  case "sequence": {
129
132
  let out = empty();
130
133
  for (const term of d.parsers) {
131
- out = union(out, fs2(term));
134
+ if (!isZeroWidthAssertion(term)) out = union(out, fs2(term));
132
135
  if (!matchesEmpty(term)) return out;
133
136
  }
134
137
  return out;
@@ -453,9 +456,9 @@ var SPACE_RANGES = [
453
456
  [12288, 12288],
454
457
  [65279, 65279]
455
458
  ];
456
- function shorthandRanges(ch) {
457
- if (ch === "d") return [[48, 57]];
458
- if (ch === "s") return SPACE_RANGES;
459
+ function shorthandRanges(ch2) {
460
+ if (ch2 === "d") return [[48, 57]];
461
+ if (ch2 === "s") return SPACE_RANGES;
459
462
  return [[48, 57], [65, 90], [97, 122], [95, 95]];
460
463
  }
461
464
  function readUnicodeEscape(body, i) {
@@ -468,9 +471,9 @@ function parseClassRanges(body) {
468
471
  const ranges = [];
469
472
  let i = 0;
470
473
  const readAtom = () => {
471
- const ch = body[i];
472
- if (ch === void 0) return null;
473
- if (ch === "\\") {
474
+ const ch2 = body[i];
475
+ if (ch2 === void 0) return null;
476
+ if (ch2 === "\\") {
474
477
  const uni = readUnicodeEscape(body, i);
475
478
  if (uni) {
476
479
  i = uni.next;
@@ -484,8 +487,8 @@ function parseClassRanges(body) {
484
487
  if (e >= "a" && e <= "z" || e >= "A" && e <= "Z") return null;
485
488
  return { cp: e.codePointAt(0) };
486
489
  }
487
- i += ch.length;
488
- return { cp: ch.codePointAt(0) };
490
+ i += ch2.length;
491
+ return { cp: ch2.codePointAt(0) };
489
492
  };
490
493
  while (i < body.length) {
491
494
  const lo = readAtom();
@@ -552,12 +555,35 @@ function shortScanner(source, flags) {
552
555
  return minOne && end === pos ? null : end;
553
556
  };
554
557
  }
558
+ function asciiCaseFold(fs2) {
559
+ if (fs2.kind !== "ranges") return fs2;
560
+ const ranges = fs2.ranges.map((r) => ({ lo: r.lo, hi: r.hi }));
561
+ const add = (lo, hi) => {
562
+ if (lo <= hi) ranges.push({ lo, hi });
563
+ };
564
+ for (const r of fs2.ranges) {
565
+ const uLo = Math.max(r.lo, 65), uHi = Math.min(r.hi, 90);
566
+ add(uLo + 32, uHi + 32);
567
+ const lLo = Math.max(r.lo, 97), lHi = Math.min(r.hi, 122);
568
+ add(lLo - 32, lHi - 32);
569
+ }
570
+ ranges.sort((a, b) => a.lo - b.lo);
571
+ const merged = [];
572
+ for (const r of ranges) {
573
+ const last = merged[merged.length - 1];
574
+ if (last && r.lo <= last.hi + 1) last.hi = Math.max(last.hi, r.hi);
575
+ else merged.push({ lo: r.lo, hi: r.hi });
576
+ }
577
+ return { kind: "ranges", ranges: merged };
578
+ }
555
579
  function regex(pattern, flags = "") {
556
580
  const source = typeof pattern === "string" ? pattern : pattern.source;
557
581
  const resolvedFlags = typeof pattern === "string" ? flags : pattern.flags;
558
582
  const anchored = new RegExp(source, "y" + resolvedFlags.replace(/[gy]/g, ""));
559
583
  const scan = shortScanner(source, resolvedFlags);
560
- const { firstSet: firstSet2, canMatchNewline } = (firstSetAnalyzer ?? permissiveFirstSet)(source);
584
+ const raw = (firstSetAnalyzer ?? permissiveFirstSet)(source);
585
+ const firstSet2 = !resolvedFlags.includes("i") ? raw.firstSet : resolvedFlags.includes("u") || resolvedFlags.includes("v") ? any() : asciiCaseFold(raw.firstSet);
586
+ const canMatchNewline = raw.canMatchNewline;
561
587
  const meta = { firstSet: firstSet2, canMatchNewline, isTrivia: false };
562
588
  return {
563
589
  _tag: "regex",
@@ -612,8 +638,8 @@ function parseRegex(src) {
612
638
  const parseSeq = () => {
613
639
  const parts = [];
614
640
  while (i < src.length) {
615
- const ch = src[i];
616
- if (ch === "|" || ch === ")") break;
641
+ const ch2 = src[i];
642
+ if (ch2 === "|" || ch2 === ")") break;
617
643
  parts.push(parseQuantified());
618
644
  }
619
645
  if (parts.length === 0) return EMPTY2;
@@ -621,18 +647,18 @@ function parseRegex(src) {
621
647
  };
622
648
  const parseQuantified = () => {
623
649
  const atom = parseAtom();
624
- const ch = src[i];
650
+ const ch2 = src[i];
625
651
  let min = null;
626
- if (ch === "*") {
652
+ if (ch2 === "*") {
627
653
  min = 0;
628
654
  i++;
629
- } else if (ch === "+") {
655
+ } else if (ch2 === "+") {
630
656
  min = 1;
631
657
  i++;
632
- } else if (ch === "?") {
658
+ } else if (ch2 === "?") {
633
659
  min = 0;
634
660
  i++;
635
- } else if (ch === "{") {
661
+ } else if (ch2 === "{") {
636
662
  min = parseBraceQuantifier();
637
663
  }
638
664
  if (min === null) return atom;
@@ -648,8 +674,8 @@ function parseRegex(src) {
648
674
  return Number.parseInt(m[1], 10);
649
675
  };
650
676
  const parseAtom = () => {
651
- const ch = src[i];
652
- switch (ch) {
677
+ const ch2 = src[i];
678
+ switch (ch2) {
653
679
  case "(":
654
680
  return parseGroup();
655
681
  case "[":
@@ -670,8 +696,8 @@ function parseRegex(src) {
670
696
  return charNode(fromRange(123, 123), false);
671
697
  // a `{` with nothing to quantify is a literal brace
672
698
  default:
673
- i += ch.length;
674
- return charNode(fromRange(ch.codePointAt(0), ch.codePointAt(0)), ch === "\n");
699
+ i += ch2.length;
700
+ return charNode(fromRange(ch2.codePointAt(0), ch2.codePointAt(0)), ch2 === "\n");
675
701
  }
676
702
  };
677
703
  const parseGroup = () => {
@@ -1104,8 +1130,8 @@ function choice(...args) {
1104
1130
  }
1105
1131
  }
1106
1132
  if (idx >= 0) {
1107
- const gate = gates[idx];
1108
- if (gate && !gate(ctx.state)) {
1133
+ const gate2 = gates[idx];
1134
+ if (gate2 && !gate2(ctx.state)) {
1109
1135
  return { ok: false, expected: deriveExpected(parsers[idx]), span: { start: pos, end: pos } };
1110
1136
  }
1111
1137
  const result = parsers[idx].parse(input, pos, ctx);
@@ -1451,11 +1477,11 @@ function tryFastLabeledScan(input, cur, trivia2) {
1451
1477
  function recordTriviaChunks(ctx, chunks) {
1452
1478
  const kinds = ctx.triviaKindLabels;
1453
1479
  const mask = ctx._triviaCaptureMask;
1454
- for (const ch of chunks) {
1455
- pushTriviaLogEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1480
+ for (const ch2 of chunks) {
1481
+ pushTriviaLogEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1456
1482
  if (ctx.captureTrivia && (ctx._cstBuf !== void 0 || ctx._cstTriviaLog !== void 0)) {
1457
- if (mask === void 0 || kinds === void 0 || (mask & 1 << ch.kindIndex) !== 0) {
1458
- pushCstTriviaEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1483
+ if (mask === void 0 || kinds === void 0 || (mask & 1 << ch2.kindIndex) !== 0) {
1484
+ pushCstTriviaEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1459
1485
  }
1460
1486
  }
1461
1487
  }
@@ -2194,6 +2220,12 @@ function rules(a, b) {
2194
2220
  if (rule && !rule._meta.isTrivia) rule._meta.grammarTrivia = options.trivia;
2195
2221
  }
2196
2222
  }
2223
+ if (options?.scanSkip != null) {
2224
+ for (const key of Object.keys(definitions)) {
2225
+ const rule = cache[key];
2226
+ if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
2227
+ }
2228
+ }
2197
2229
  for (const key of Object.keys(definitions)) {
2198
2230
  markUnusedValues(cache[key]);
2199
2231
  }
@@ -2762,6 +2794,413 @@ function leaf(root, fn) {
2762
2794
  };
2763
2795
  }
2764
2796
 
2797
+ // src/version.ts
2798
+ var PARSEMAN_VERSION = "0.33.0";
2799
+
2800
+ // src/analysis/gating.ts
2801
+ var isAny = (fs2) => fs2.kind === "any";
2802
+ function intersects2(a, b) {
2803
+ if (a.kind === "any" || b.kind === "any") return true;
2804
+ if (a.kind === "empty" || b.kind === "empty") return false;
2805
+ for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
2806
+ return false;
2807
+ }
2808
+ function intersection(a, b) {
2809
+ if (a.kind === "any") return b;
2810
+ if (b.kind === "any") return a;
2811
+ if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
2812
+ const ranges = [];
2813
+ for (const ra of a.ranges) for (const rb of b.ranges) {
2814
+ const lo = Math.max(ra.lo, rb.lo);
2815
+ const hi = Math.min(ra.hi, rb.hi);
2816
+ if (lo <= hi) ranges.push({ lo, hi });
2817
+ }
2818
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
2819
+ }
2820
+ function combine(sets) {
2821
+ let acc = { kind: "empty" };
2822
+ for (const s of sets) {
2823
+ if (s.kind === "any") return { kind: "any" };
2824
+ if (s.kind === "empty") continue;
2825
+ acc = acc.kind === "ranges" ? { kind: "ranges", ranges: [...acc.ranges, ...s.ranges] } : s;
2826
+ }
2827
+ return acc;
2828
+ }
2829
+ var ch = (c) => c >= 32 && c < 127 ? `'${String.fromCharCode(c)}'` : `\\u${c}`;
2830
+ function firstSetToString(fs2) {
2831
+ if (fs2.kind === "any") return "ANY";
2832
+ if (fs2.kind === "empty") return "(empty)";
2833
+ return fs2.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
2834
+ }
2835
+ var ruleNameOf = (p) => p._ruleName;
2836
+ var SUGGESTIONS = {
2837
+ "broad-recognizer": "if this arm leads with a keyword regex, use word('kw', boundary) / keywords([...]) \u2014 they expose an EXACT, gating first-set and lower to the same charCodeAt scan. A genuine scanTo fallback is fine; accept it in the gating snapshot if intentional.",
2838
+ "leading-not": "let the arm lead with its actual consuming terminal (first-sets gate it automatically); keep not(...) only as a TRAILING boundary. Do not hand-roll not(not(...)) to fake gating.",
2839
+ "nullable-prefix": "a leading optional/many lets a later, broad term start the arm. Split the empty case into its own arm, or gate on the prefix first char.",
2840
+ "cross-artifact-ref": "parseman >=0.32.0 resolves a g.Foo ref first-set at fuse time; if still ANY the target rule is itself ungated \u2014 analyze it and give it a concrete non-nullable lead.",
2841
+ "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2842
+ "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2843
+ };
2844
+ function classifyBroadArm(arm) {
2845
+ const seen = /* @__PURE__ */ new Set();
2846
+ const walk2 = (p) => {
2847
+ if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2848
+ seen.add(p);
2849
+ const d = p._def;
2850
+ switch (d.tag) {
2851
+ case "literal":
2852
+ case "regex":
2853
+ case "keywords":
2854
+ return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2855
+ case "not":
2856
+ return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
2857
+ case "scanTo":
2858
+ return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2859
+ // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
2860
+ // its own — it is the poison. `withCtx`/`recover` FORWARD their inner
2861
+ // first-set (see firstSetOf), so a broad result comes from the inner parser:
2862
+ // walk into it for a precise cause.
2863
+ case "guard":
2864
+ return { cause: "opaque-wrapper", detail: `opaque wrapper (${d.tag})` };
2865
+ // attempt/withCtx/recover FORWARD their inner first-set (transparent for
2866
+ // dispatch), so a broad result comes from the inner parser — walk into it.
2867
+ case "attempt":
2868
+ case "withCtx":
2869
+ case "recover":
2870
+ return walk2(d.parser);
2871
+ case "lazy": {
2872
+ const name = p._ruleName;
2873
+ try {
2874
+ const inner = walk2(d.thunk());
2875
+ if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
2876
+ return inner;
2877
+ } catch {
2878
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
2879
+ }
2880
+ }
2881
+ case "optional":
2882
+ case "many": {
2883
+ const inner = walk2(d.parser);
2884
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
2885
+ }
2886
+ case "oneOrMore":
2887
+ case "transform":
2888
+ case "label":
2889
+ case "field":
2890
+ case "trivia":
2891
+ case "token":
2892
+ case "leaf":
2893
+ case "node":
2894
+ case "grammar":
2895
+ case "expect":
2896
+ return walk2(d.parser);
2897
+ case "skip":
2898
+ return walk2(d.main);
2899
+ case "sequence": {
2900
+ let sawNullablePrefix = false;
2901
+ for (const t of d.parsers) {
2902
+ if (t._def.tag === "not") {
2903
+ sawNullablePrefix = true;
2904
+ continue;
2905
+ }
2906
+ if (isAny(firstSetOf(t))) {
2907
+ const inner = walk2(t);
2908
+ if (sawNullablePrefix)
2909
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
2910
+ return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2911
+ }
2912
+ if (matchesEmpty(t)) {
2913
+ sawNullablePrefix = true;
2914
+ continue;
2915
+ }
2916
+ return null;
2917
+ }
2918
+ return { cause: "broad-recognizer", detail: "sequence of only nullable/zero-width terms" };
2919
+ }
2920
+ case "choice": {
2921
+ for (const a of d.parsers) {
2922
+ const r = walk2(a);
2923
+ if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
2924
+ }
2925
+ return null;
2926
+ }
2927
+ default:
2928
+ return { cause: "broad-recognizer", detail: `unmodeled construct (${d.tag})` };
2929
+ }
2930
+ };
2931
+ return walk2(arm) ?? { cause: "broad-recognizer", detail: "broad (cause not localized)" };
2932
+ }
2933
+ function peelToLeading(arm) {
2934
+ let d = arm._def;
2935
+ let cur = arm;
2936
+ for (; ; ) {
2937
+ if (d.tag === "node" || d.tag === "grammar" || d.tag === "transform" || d.tag === "label" || d.tag === "attempt") {
2938
+ cur = d.parser;
2939
+ d = cur._def;
2940
+ continue;
2941
+ }
2942
+ break;
2943
+ }
2944
+ if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
2945
+ return cur;
2946
+ }
2947
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
2948
+ function detectAntiPatterns(rule, arms) {
2949
+ const out = [];
2950
+ arms.forEach((arm, i) => {
2951
+ const lead = peelToLeading(arm);
2952
+ const ld = lead._def;
2953
+ if (ld.tag === "not") {
2954
+ const inner = ld.parser;
2955
+ if (inner._def.tag === "not") {
2956
+ out.push({
2957
+ kind: "double-not",
2958
+ rule,
2959
+ armIndex: i,
2960
+ message: "not(not(...)) hand-rolls automatic first-char gating and MISCOMPILES among shared-first-char sibling arms (its first-set is ANY, poisoning dispatch). Remove it; let the arm lead with its consuming terminal, or use word()/keywords() for an exact first-set."
2961
+ });
2962
+ } else {
2963
+ out.push({
2964
+ kind: "leading-not",
2965
+ rule,
2966
+ armIndex: i,
2967
+ message: "a leading not(...) on a choice arm hand-rolls gating and poisons the choice first-set (not() is ANY). Reorder so a consuming terminal leads; keep not(...) as a TRAILING boundary only."
2968
+ });
2969
+ }
2970
+ }
2971
+ if (ld.tag === "regex" && KEYWORD_REGEX_RE.test(ld.source)) {
2972
+ out.push({
2973
+ kind: "keyword-regex",
2974
+ rule,
2975
+ armIndex: i,
2976
+ message: `regex(/${ld.source}/) leads this arm but is a keyword \u2014 use word('\u2026', boundary) / keywords([\u2026]) for an EXACT resolvable first-set (they lower to the same charCodeAt scan).`
2977
+ });
2978
+ }
2979
+ });
2980
+ return out;
2981
+ }
2982
+ function analyzeGating(entry, opts) {
2983
+ const raw = [];
2984
+ const antiPatterns = [];
2985
+ const visited = /* @__PURE__ */ new Set();
2986
+ const visit = (p, enclosingRule) => {
2987
+ if (visited.has(p)) return;
2988
+ visited.add(p);
2989
+ const d = p._def;
2990
+ const rule = ruleNameOf(p) ?? enclosingRule;
2991
+ if (d.tag === "choice") {
2992
+ raw.push({ g: analyzeChoice(p, d, rule), rule });
2993
+ antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
2994
+ }
2995
+ const rec = d;
2996
+ const kids = [];
2997
+ if (Array.isArray(rec.parsers)) kids.push(...rec.parsers);
2998
+ for (const k of ["parser", "main", "skipped", "separator", "sentinel"])
2999
+ if (rec[k]) kids.push(rec[k]);
3000
+ if (Array.isArray(rec.skip)) kids.push(...rec.skip);
3001
+ if (d.tag === "lazy") {
3002
+ try {
3003
+ kids.push(d.thunk());
3004
+ } catch {
3005
+ }
3006
+ }
3007
+ for (const k of kids) visit(k, rule);
3008
+ };
3009
+ visit(entry, ruleNameOf(entry) ?? "<entry>");
3010
+ const perRule = /* @__PURE__ */ new Map();
3011
+ for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3012
+ const seenInRule = /* @__PURE__ */ new Map();
3013
+ const accept = new Set(opts?.accept ?? []);
3014
+ const usedAccept = /* @__PURE__ */ new Set();
3015
+ const choices = raw.map(({ g, rule }) => {
3016
+ const n = seenInRule.get(rule) ?? 0;
3017
+ seenInRule.set(rule, n + 1);
3018
+ const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
3019
+ const isAccepted = g.gates === "no" && accept.has(id);
3020
+ if (isAccepted) usedAccept.add(id);
3021
+ return { ...g, id, accepted: isAccepted };
3022
+ });
3023
+ const gated = choices.filter((c) => c.gates === "yes").length;
3024
+ const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3025
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3026
+ const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3027
+ const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3028
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3029
+ }
3030
+ function analyzeChoice(p, d, rule) {
3031
+ const arms = d.parsers;
3032
+ const shallow = arms.map((a) => a._meta.firstSet);
3033
+ const deep = arms.map((a) => firstSetOf(a));
3034
+ const anyArms = deep.map((fs2, index) => ({ fs: fs2, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3035
+ const { cause, detail } = classifyBroadArm(arms[index]);
3036
+ return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3037
+ });
3038
+ const overlaps = [];
3039
+ for (let i = 0; i < deep.length; i++)
3040
+ for (let j = i + 1; j < deep.length; j++)
3041
+ if (!isAny(deep[i]) && !isAny(deep[j]) && intersects2(deep[i], deep[j]))
3042
+ overlaps.push({
3043
+ a: i,
3044
+ b: j,
3045
+ on: intersection(deep[i], deep[j]),
3046
+ suggestion: "arms share a first char \u2014 left-factor. parseman auto-detects the sharedPrefix strategy for bare sequences (choice.ts); make the arms bare sequences with a common leading terminal, or restructure."
3047
+ });
3048
+ let gates;
3049
+ if (d.disjoint) gates = "yes";
3050
+ else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3051
+ else gates = "no";
3052
+ return {
3053
+ rule,
3054
+ strategy: d.strategy?.tag ?? "firstMatch",
3055
+ gates,
3056
+ combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3057
+ anyArms,
3058
+ overlaps
3059
+ };
3060
+ }
3061
+ function formatGatingWarnings(report) {
3062
+ const lines = [];
3063
+ for (const c of report.ungated) {
3064
+ lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
3065
+ for (const a of c.anyArms)
3066
+ lines.push(` \xB7 arm[${a.index}] first-set ANY (${a.cause}): ${a.detail}
3067
+ fix: ${a.suggestion}`);
3068
+ for (const o of c.overlaps)
3069
+ lines.push(` \xB7 arm[${o.a}] \u2229 arm[${o.b}] overlap on ${firstSetToString(o.on)}
3070
+ fix: ${o.suggestion}`);
3071
+ lines.push(` (intentional? accept it in the gating snapshot: { accept: ['${c.id}'] }.)`);
3072
+ }
3073
+ for (const ap of report.antiPatterns)
3074
+ lines.push(`parseman anti-pattern [${ap.kind}] @ ${ap.rule} arm[${ap.armIndex}]: ${ap.message}`);
3075
+ return lines;
3076
+ }
3077
+
3078
+ // src/combinators/scanTo.ts
3079
+ function resolveScanSkip(explicitSkip, raw, ctx) {
3080
+ if (raw) return [];
3081
+ const trivia2 = ctx.trivia;
3082
+ const ambient = ctx.scanSkip;
3083
+ if (!trivia2 && !ambient) return explicitSkip;
3084
+ const out = [];
3085
+ if (trivia2) out.push(trivia2);
3086
+ if (ambient) out.push(...ambient);
3087
+ out.push(...explicitSkip);
3088
+ return out;
3089
+ }
3090
+ function scanTo(sentinel, { skip: skip2, raw = false, orEOF = false } = {}) {
3091
+ const meta = {
3092
+ firstSet: any(),
3093
+ canMatchNewline: true,
3094
+ isTrivia: false
3095
+ };
3096
+ const explicitSkip = skip2 ?? [];
3097
+ return {
3098
+ _tag: "scanTo",
3099
+ _meta: meta,
3100
+ _def: { tag: "scanTo", sentinel, skip: explicitSkip, raw, orEOF },
3101
+ parse(input, pos, ctx) {
3102
+ let cur = pos;
3103
+ const skip3 = resolveScanSkip(explicitSkip, raw, ctx);
3104
+ const probeCtx = {
3105
+ trackLines: ctx.trackLines,
3106
+ state: ctx.state,
3107
+ ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
3108
+ };
3109
+ const emit2 = (end) => {
3110
+ if (end > pos && cstCaptureActive(ctx)) {
3111
+ const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
3112
+ pushCstLeaf(ctx, leaf2);
3113
+ }
3114
+ };
3115
+ while (cur < input.length) {
3116
+ const s = sentinel.parse(input, cur, probeCtx);
3117
+ if (s.ok) {
3118
+ emit2(cur);
3119
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3120
+ }
3121
+ let advanced = false;
3122
+ for (const skipper of skip3) {
3123
+ const r = skipper.parse(input, cur, probeCtx);
3124
+ if (r.ok && r.span.end > cur) {
3125
+ cur = r.span.end;
3126
+ advanced = true;
3127
+ break;
3128
+ }
3129
+ }
3130
+ if (!advanced) cur++;
3131
+ }
3132
+ if (orEOF) {
3133
+ emit2(cur);
3134
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3135
+ }
3136
+ const sentDef = sentinel._def;
3137
+ const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
3138
+ return { ok: false, expected, span: { start: pos, end: cur } };
3139
+ }
3140
+ };
3141
+ }
3142
+ function balanced(open, close, options = {}) {
3143
+ const ownSkip = options.skip ?? [];
3144
+ const combi = buildBalancedInterior(open, close, ownSkip);
3145
+ if (options.raw) return combi;
3146
+ const eagerParse = combi.parse.bind(combi);
3147
+ const cache = /* @__PURE__ */ new Map();
3148
+ combi._balancedAmbient = { open, close, ownSkip };
3149
+ combi.parse = (input, pos, ctx) => {
3150
+ const amb = ctx.scanSkip;
3151
+ if (!amb || amb.length === 0) return eagerParse(input, pos, ctx);
3152
+ let interior = cache.get(amb);
3153
+ if (!interior) {
3154
+ interior = buildBalancedInterior(open, close, [...amb, ...ownSkip]);
3155
+ cache.set(amb, interior);
3156
+ }
3157
+ return interior.parse(input, pos, ctx);
3158
+ };
3159
+ return combi;
3160
+ }
3161
+ function buildBalancedInterior(open, close, skips) {
3162
+ const self = ref();
3163
+ const stop = /* @__PURE__ */ new Set([open, close]);
3164
+ let bounded = true;
3165
+ for (const sk of skips) {
3166
+ const cs = firstSetClassChars(sk);
3167
+ if (cs === null) {
3168
+ bounded = false;
3169
+ break;
3170
+ }
3171
+ for (const ch2 of cs) stop.add(ch2);
3172
+ }
3173
+ const cls = [...stop].map(escapeClassChar).join("");
3174
+ const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
3175
+ const inner = many(choice(self, ...skips, content));
3176
+ const combi = transform(
3177
+ sequence(literal(open), inner, expect(literal(close))),
3178
+ // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
3179
+ // ParseError (recovered close). `c` is the close string or a ParseError.
3180
+ ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
3181
+ );
3182
+ if (combi._def.tag === "transform") {
3183
+ combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
3184
+ combi._def.recognitionOnly = true;
3185
+ }
3186
+ self.define(combi);
3187
+ return combi;
3188
+ }
3189
+ function firstSetClassChars(c) {
3190
+ const fs2 = c._meta.firstSet;
3191
+ if (fs2.kind === "empty") return [];
3192
+ if (fs2.kind !== "ranges") return null;
3193
+ const out = [];
3194
+ for (const { lo, hi } of fs2.ranges) {
3195
+ if (hi - lo > 8) return null;
3196
+ for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
3197
+ }
3198
+ return out;
3199
+ }
3200
+ function escapeClassChar(ch2) {
3201
+ return ch2.replace(/[\\\]^-]/g, "\\$&");
3202
+ }
3203
+
2765
3204
  // src/compiler/grammar-coverage-ids.ts
2766
3205
  function children(def, winners) {
2767
3206
  switch (def.tag) {
@@ -2874,16 +3313,16 @@ function literalCodePoints(frag) {
2874
3313
  const out = [];
2875
3314
  let i = 0;
2876
3315
  while (i < frag.length) {
2877
- const ch = frag[i];
2878
- if (ch === "\\") {
3316
+ const ch2 = frag[i];
3317
+ if (ch2 === "\\") {
2879
3318
  const e = frag[i + 1];
2880
3319
  if (e === void 0) return null;
2881
3320
  out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e] : e.codePointAt(0));
2882
3321
  i += 2;
2883
3322
  continue;
2884
3323
  }
2885
- if (META.has(ch)) return null;
2886
- out.push(ch.codePointAt(0));
3324
+ if (META.has(ch2)) return null;
3325
+ out.push(ch2.codePointAt(0));
2887
3326
  i += 1;
2888
3327
  }
2889
3328
  return out.length ? out : null;
@@ -2962,8 +3401,8 @@ function parseSeqParts(source) {
2962
3401
  };
2963
3402
  let i = 0;
2964
3403
  while (i < source.length) {
2965
- const ch = source[i];
2966
- if (ch === "[") {
3404
+ const ch2 = source[i];
3405
+ if (ch2 === "[") {
2967
3406
  let k = i + 1;
2968
3407
  const neg = source[k] === "^";
2969
3408
  if (neg) k++;
@@ -2985,7 +3424,7 @@ function parseSeqParts(source) {
2985
3424
  i += runFrom(ranges, neg, i);
2986
3425
  continue;
2987
3426
  }
2988
- if (ch === "\\") {
3427
+ if (ch2 === "\\") {
2989
3428
  const e = source[i + 1];
2990
3429
  if (e === "d" || e === "w" || e === "s") {
2991
3430
  flush();
@@ -3014,7 +3453,7 @@ function parseSeqParts(source) {
3014
3453
  } else lit.push(cp2);
3015
3454
  continue;
3016
3455
  }
3017
- if (ch === "(") {
3456
+ if (ch2 === "(") {
3018
3457
  if (source[i + 1] !== "?" || source[i + 2] !== ":") return null;
3019
3458
  let k = i + 3;
3020
3459
  let depth = 1;
@@ -3063,9 +3502,9 @@ function parseSeqParts(source) {
3063
3502
  }
3064
3503
  continue;
3065
3504
  }
3066
- if (META.has(ch)) return null;
3067
- const cp = ch.codePointAt(0);
3068
- i += ch.length;
3505
+ if (META.has(ch2)) return null;
3506
+ const cp = ch2.codePointAt(0);
3507
+ i += ch2.length;
3069
3508
  if (source[i] === "?") {
3070
3509
  flush();
3071
3510
  parts.push({ part: "lit", cps: [cp], optional: true });
@@ -3242,12 +3681,12 @@ function unwrapOuterGroup(source) {
3242
3681
  let i = 3;
3243
3682
  let ok = true;
3244
3683
  while (i < source.length && depth > 0) {
3245
- const ch = source[i];
3246
- if (ch === "\\") {
3684
+ const ch2 = source[i];
3685
+ if (ch2 === "\\") {
3247
3686
  i += 2;
3248
3687
  continue;
3249
3688
  }
3250
- if (ch === "[") {
3689
+ if (ch2 === "[") {
3251
3690
  let j = i + 1;
3252
3691
  if (source[j] === "^") j++;
3253
3692
  while (j < source.length && source[j] !== "]") {
@@ -3261,8 +3700,8 @@ function unwrapOuterGroup(source) {
3261
3700
  i = j + 1;
3262
3701
  continue;
3263
3702
  }
3264
- if (ch === "(") depth++;
3265
- else if (ch === ")") depth--;
3703
+ if (ch2 === "(") depth++;
3704
+ else if (ch2 === ")") depth--;
3266
3705
  i++;
3267
3706
  }
3268
3707
  if (!ok || depth !== 0 || i !== source.length) break;
@@ -3277,12 +3716,12 @@ function splitTopLevelAlternation(source) {
3277
3716
  let last = 0;
3278
3717
  let i = 0;
3279
3718
  while (i < body.length) {
3280
- const ch = body[i];
3281
- if (ch === "\\") {
3719
+ const ch2 = body[i];
3720
+ if (ch2 === "\\") {
3282
3721
  i += 2;
3283
3722
  continue;
3284
3723
  }
3285
- if (ch === "[") {
3724
+ if (ch2 === "[") {
3286
3725
  let j = i + 1;
3287
3726
  if (body[j] === "^") j++;
3288
3727
  while (j < body.length && body[j] !== "]") {
@@ -3293,18 +3732,18 @@ function splitTopLevelAlternation(source) {
3293
3732
  i = j + 1;
3294
3733
  continue;
3295
3734
  }
3296
- if (ch === "(") {
3735
+ if (ch2 === "(") {
3297
3736
  depth++;
3298
3737
  i++;
3299
3738
  continue;
3300
3739
  }
3301
- if (ch === ")") {
3740
+ if (ch2 === ")") {
3302
3741
  depth--;
3303
3742
  if (depth < 0) return null;
3304
3743
  i++;
3305
3744
  continue;
3306
3745
  }
3307
- if (ch === "|" && depth === 0) {
3746
+ if (ch2 === "|" && depth === 0) {
3308
3747
  parts.push(body.slice(last, i));
3309
3748
  last = i + 1;
3310
3749
  i++;
@@ -3423,12 +3862,12 @@ function splitDelimArms(body) {
3423
3862
  let last = 0;
3424
3863
  let i = 0;
3425
3864
  while (i < body.length) {
3426
- const ch = body[i];
3427
- if (ch === "\\") {
3865
+ const ch2 = body[i];
3866
+ if (ch2 === "\\") {
3428
3867
  i += 2;
3429
3868
  continue;
3430
3869
  }
3431
- if (ch === "[") {
3870
+ if (ch2 === "[") {
3432
3871
  let j = i + 1;
3433
3872
  if (body[j] === "^") j++;
3434
3873
  while (j < body.length && body[j] !== "]") {
@@ -3439,18 +3878,18 @@ function splitDelimArms(body) {
3439
3878
  i = j + 1;
3440
3879
  continue;
3441
3880
  }
3442
- if (ch === "(") {
3881
+ if (ch2 === "(") {
3443
3882
  depth++;
3444
3883
  i++;
3445
3884
  continue;
3446
3885
  }
3447
- if (ch === ")") {
3886
+ if (ch2 === ")") {
3448
3887
  depth--;
3449
3888
  if (depth < 0) return null;
3450
3889
  i++;
3451
3890
  continue;
3452
3891
  }
3453
- if (ch === "|" && depth === 0) {
3892
+ if (ch2 === "|" && depth === 0) {
3454
3893
  arms.push(body.slice(last, i));
3455
3894
  last = i + 1;
3456
3895
  }
@@ -3502,12 +3941,12 @@ function parseDelimited(source) {
3502
3941
  let k = i + 3;
3503
3942
  let depth = 1;
3504
3943
  while (k < source.length && depth > 0) {
3505
- const ch = source[k];
3506
- if (ch === "\\") {
3944
+ const ch2 = source[k];
3945
+ if (ch2 === "\\") {
3507
3946
  k += 2;
3508
3947
  continue;
3509
3948
  }
3510
- if (ch === "[") {
3949
+ if (ch2 === "[") {
3511
3950
  let j = k + 1;
3512
3951
  if (source[j] === "^") j++;
3513
3952
  while (j < source.length && source[j] !== "]") {
@@ -3518,8 +3957,8 @@ function parseDelimited(source) {
3518
3957
  k = j + 1;
3519
3958
  continue;
3520
3959
  }
3521
- if (ch === "(") depth++;
3522
- else if (ch === ")") depth--;
3960
+ if (ch2 === "(") depth++;
3961
+ else if (ch2 === ")") depth--;
3523
3962
  k++;
3524
3963
  }
3525
3964
  if (depth !== 0) return null;
@@ -4191,58 +4630,88 @@ function tryInlineDestructureTransform(src, valueVars) {
4191
4630
 
4192
4631
  // src/compiler/codegen.ts
4193
4632
  function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
4194
- if (seen.has(p)) return { concrete: empty(), refs: [] };
4195
- seen.add(p);
4196
4633
  const d = p._def;
4634
+ if (d.tag === "lazy") {
4635
+ const name = p._ruleName;
4636
+ if (name !== void 0) return { alts: [[{ set: empty(), nullable: false, ref: name }]] };
4637
+ }
4638
+ if (seen.has(p)) return { alts: [[]] };
4639
+ seen.add(p);
4197
4640
  const rec = (c) => leadingFirstSetRecipe(c, seen);
4198
- const merge = (a, b) => ({ concrete: union(a.concrete, b.concrete), refs: [...a.refs, ...b.refs] });
4199
- switch (d.tag) {
4200
- case "lazy": {
4201
- const name = p._ruleName;
4202
- if (name !== void 0) return { concrete: empty(), refs: [name] };
4203
- try {
4204
- return rec(d.thunk());
4205
- } catch {
4206
- return { concrete: any(), refs: [] };
4641
+ const seg = (set, nullable2) => ({ set, nullable: nullable2 });
4642
+ const forceNullable = (r) => ({ alts: r.alts.map((alt) => alt.map((s) => ({ ...s, nullable: true }))) });
4643
+ const collapse = (r, nullable2) => {
4644
+ let set = empty();
4645
+ let hasRef = false;
4646
+ for (const alt of r.alts) {
4647
+ for (const s of alt) {
4648
+ if (s.ref !== void 0) hasRef = true;
4649
+ set = union(set, s.set);
4650
+ if (!s.nullable) break;
4651
+ }
4652
+ }
4653
+ return seg(hasRef ? any() : set, nullable2);
4654
+ };
4655
+ const compute = () => {
4656
+ switch (d.tag) {
4657
+ case "lazy": {
4658
+ try {
4659
+ return rec(d.thunk());
4660
+ } catch {
4661
+ return { alts: [[seg(any(), false)]] };
4662
+ }
4207
4663
  }
4208
- }
4209
- case "literal":
4210
- case "regex":
4211
- case "keywords":
4212
- return { concrete: p._meta.firstSet, refs: [] };
4213
- case "choice": {
4214
- let out = { concrete: empty(), refs: [] };
4215
- for (const arm of d.parsers) out = merge(out, rec(arm));
4216
- return out;
4217
- }
4218
- case "sequence": {
4219
- let out = { concrete: empty(), refs: [] };
4220
- for (const term of d.parsers) {
4221
- out = merge(out, rec(term));
4222
- if (!matchesEmpty(term)) return out;
4664
+ case "literal":
4665
+ case "regex":
4666
+ case "keywords":
4667
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4668
+ case "choice": {
4669
+ const alts = [];
4670
+ for (const arm of d.parsers) alts.push(...rec(arm).alts);
4671
+ return { alts };
4672
+ }
4673
+ case "sequence": {
4674
+ const chain = [];
4675
+ for (const term of d.parsers) {
4676
+ if (!isZeroWidthAssertion(term)) {
4677
+ const tr = rec(term);
4678
+ if (tr.alts.length === 1) chain.push(...tr.alts[0]);
4679
+ else chain.push(collapse(tr, matchesEmpty(term)));
4680
+ }
4681
+ if (!matchesEmpty(term)) return { alts: [chain] };
4682
+ }
4683
+ return { alts: [chain] };
4223
4684
  }
4224
- return out;
4685
+ // `many`/`optional` are skippable as a whole → their leading chars start the
4686
+ // sequence but the chain must continue past them (force nullable). `oneOrMore`
4687
+ // requires at least one repetition, so it is NOT made nullable here.
4688
+ case "many":
4689
+ case "optional":
4690
+ return forceNullable(rec(d.parser));
4691
+ case "oneOrMore":
4692
+ case "transform":
4693
+ case "label":
4694
+ case "field":
4695
+ case "trivia":
4696
+ case "token":
4697
+ case "leaf":
4698
+ case "node":
4699
+ case "grammar":
4700
+ case "expect":
4701
+ return rec(d.parser);
4702
+ case "sepBy":
4703
+ return rec(d.parser);
4704
+ case "skip":
4705
+ return rec(d.main);
4706
+ // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4707
+ default:
4708
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4225
4709
  }
4226
- case "oneOrMore":
4227
- case "many":
4228
- case "optional":
4229
- case "transform":
4230
- case "label":
4231
- case "field":
4232
- case "trivia":
4233
- case "token":
4234
- case "leaf":
4235
- case "node":
4236
- case "grammar":
4237
- case "expect":
4238
- return rec(d.parser);
4239
- case "sepBy":
4240
- return rec(d.parser);
4241
- case "skip":
4242
- return rec(d.main);
4243
- // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4244
- default:
4245
- return { concrete: p._meta.firstSet, refs: [] };
4710
+ };
4711
+ try {
4712
+ return compute();
4713
+ } finally {
4714
+ seen.delete(p);
4246
4715
  }
4247
4716
  }
4248
4717
  var HOST_READS_DECL = "const _hostReads = (b, n) => { if (b === undefined) return false; let s; try { s = Function.prototype.toString.call(b) } catch (e) { return true } if (/\\barguments\\b/.test(s)) return true; const m = /^[^(]*\\(([\\s\\S]*?)\\)/.exec(s); if (m && /\\.\\.\\.|=/.test(m[1])) return true; return b.length > n }";
@@ -4446,16 +4915,19 @@ function ensureTriviaFn(ctx) {
4446
4915
  const savedIndent = ctx.indent;
4447
4916
  const savedFailLabel = ctx.failLabel;
4448
4917
  const savedTrivia = ctx.activeTrivia;
4918
+ const savedScanSkip = ctx.activeScanSkip;
4449
4919
  const savedCapAsTrivia = ctx.capAsTrivia;
4450
4920
  ctx.indent = 2;
4451
4921
  ctx.failLabel = "_triv";
4452
4922
  ctx.capAsTrivia = true;
4453
4923
  ctx.activeTrivia = void 0;
4924
+ ctx.activeScanSkip = void 0;
4454
4925
  const r = emit(trivia2, ctx, "_pos");
4455
4926
  ctx.indent = savedIndent;
4456
4927
  ctx.failLabel = savedFailLabel;
4457
4928
  ctx.capAsTrivia = savedCapAsTrivia;
4458
4929
  ctx.activeTrivia = savedTrivia;
4930
+ ctx.activeScanSkip = savedScanSkip;
4459
4931
  ctx.namedFnDecls.push([
4460
4932
  `function ${fnName}(input, _pos, _ctx, _cap) {`,
4461
4933
  ` let _e = _pos`,
@@ -4733,7 +5205,7 @@ function emitKeywordsFast(def, ctx, pos) {
4733
5205
  const bodyInd = ind(ctx) + " ";
4734
5206
  const tries = [];
4735
5207
  for (const w of def.words) {
4736
- const cps = Array.from(w, (ch) => ch.codePointAt(0));
5208
+ const cps = Array.from(w, (ch2) => ch2.codePointAt(0));
4737
5209
  let shape = def.caseInsensitive ? { kind: "litFold", open: cps } : { kind: "seq", parts: [{ part: "lit", cps, optional: false }] };
4738
5210
  if (boundary) {
4739
5211
  shape = { kind: "lookahead", inner: shape, ranges: boundary.ranges, classNegated: boundary.negated, negative: true };
@@ -4926,7 +5398,7 @@ function canMatchEmptyAtStart(p) {
4926
5398
  const d = p._def;
4927
5399
  switch (d.tag) {
4928
5400
  case "regex":
4929
- return /(?:[*?]|\{0,|\{\d*,)/.test(d.source);
5401
+ return regexMatchesEmpty(d.source, d.flags);
4930
5402
  case "optional":
4931
5403
  return true;
4932
5404
  case "many":
@@ -4978,10 +5450,10 @@ function emitDisjointArm(p, ctx, pos, valV, endV, coverageId) {
4978
5450
  }
4979
5451
  function emitDisjointArmGated(def, i, ctx, pos, valV, endV, coverageId) {
4980
5452
  const p = def.parsers[i];
4981
- const gate = def.gates[i];
4982
- if (!gate) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
5453
+ const gate2 = def.gates[i];
5454
+ if (!gate2) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
4983
5455
  const gateIdx = ctx.mapFns.length;
4984
- ctx.mapFns.push(gate);
5456
+ ctx.mapFns.push(gate2);
4985
5457
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
4986
5458
  const gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
4987
5459
  const stmts = [`${ind(ctx)}if (${gateCond}) {`];
@@ -5139,7 +5611,7 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5139
5611
  if (preStmts) stmts.push(...preStmts);
5140
5612
  for (let i = 0; i < def.parsers.length; i++) {
5141
5613
  const p = def.parsers[i];
5142
- const gate = def.gates[i];
5614
+ const gate2 = def.gates[i];
5143
5615
  const autoNot = def.autoNot[i];
5144
5616
  const atStart = failsAtStart(p);
5145
5617
  const staticFx = armStaticExpected(ctx, p);
@@ -5151,9 +5623,9 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5151
5623
  })();
5152
5624
  const fsGuard = armGuardPrefix === void 0 ? baseFsGuard : baseFsGuard === null ? armGuardPrefix : `${armGuardPrefix} && (${baseFsGuard})`;
5153
5625
  let gateCond = null;
5154
- if (gate) {
5626
+ if (gate2) {
5155
5627
  const gateIdx = ctx.mapFns.length;
5156
- ctx.mapFns.push(gate);
5628
+ ctx.mapFns.push(gate2);
5157
5629
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5158
5630
  gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5159
5631
  }
@@ -5570,10 +6042,15 @@ function emitScanTo(def, ctx, pos) {
5570
6042
  const probeCtx = { ...ctx, capturing: false, noHoist: true };
5571
6043
  const { stmts: sentStmts, okVar: sentOk } = emitFallible(def.sentinel, probeCtx, curV);
5572
6044
  stmts.push(...sentStmts, `${ind(ctx)}if (${sentOk}) { ${foundV} = true; break }`);
5573
- if (def.skip.length > 0) {
6045
+ const effectiveSkip = def.raw ? def.skip : [
6046
+ ...ctx.activeTrivia ? [ctx.activeTrivia] : [],
6047
+ ...ctx.activeScanSkip ?? [],
6048
+ ...def.skip
6049
+ ];
6050
+ if (effectiveSkip.length > 0) {
5574
6051
  const advV = v(ctx, "_stadv");
5575
6052
  stmts.push(`${ind(ctx)}let ${advV} = false`);
5576
- for (const skipper of def.skip) {
6053
+ for (const skipper of effectiveSkip) {
5577
6054
  const { stmts: skStmts, okVar: skOk, endVar: skEnd } = emitFallible(skipper, probeCtx, curV);
5578
6055
  stmts.push(
5579
6056
  `${ind(ctx)}if (!${advV}) {`,
@@ -6043,6 +6520,24 @@ function emit(p, ctx, pos) {
6043
6520
  return instrument(dispatch());
6044
6521
  }
6045
6522
  function emitDispatch(p, ctx, pos) {
6523
+ const bal = p._balancedAmbient;
6524
+ if (bal && ctx.activeScanSkip && ctx.activeScanSkip.length > 0 && !ctx.balancedRebuildStack?.has(p)) {
6525
+ const interior = buildBalancedInterior(bal.open, bal.close, [...ctx.activeScanSkip, ...bal.ownSkip]);
6526
+ markUnusedValues(interior);
6527
+ if (ctx.lazyUsage) {
6528
+ const u = analyzeLazyUsage(interior);
6529
+ for (const [k, n] of u.counts) ctx.lazyUsage.counts.set(k, (ctx.lazyUsage.counts.get(k) ?? 0) + n);
6530
+ for (const k of u.recursive) ctx.lazyUsage.recursive.add(k);
6531
+ for (const [k, s] of u.sizes) ctx.lazyUsage.sizes.set(k, s);
6532
+ }
6533
+ const stack = ctx.balancedRebuildStack ?? (ctx.balancedRebuildStack = /* @__PURE__ */ new Set());
6534
+ stack.add(p);
6535
+ try {
6536
+ return emit(interior, ctx, pos);
6537
+ } finally {
6538
+ stack.delete(p);
6539
+ }
6540
+ }
6046
6541
  const def = p._def;
6047
6542
  switch (def.tag) {
6048
6543
  case "literal":
@@ -6464,9 +6959,36 @@ function ruleDependencies(ruleMap) {
6464
6959
  }
6465
6960
  return deps;
6466
6961
  }
6962
+ function resolveGatingLevel(opt) {
6963
+ const explicit = typeof opt === "string" ? opt : opt?.level;
6964
+ if (explicit !== void 0) return explicit;
6965
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_GATING : void 0;
6966
+ if (env === "off" || env === "warn" || env === "error") return env;
6967
+ return "warn";
6968
+ }
6969
+ function runGatingDiagnostic(combinator, opt) {
6970
+ const level = resolveGatingLevel(opt);
6971
+ if (level === "off") return void 0;
6972
+ const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
6973
+ let report;
6974
+ try {
6975
+ report = analyzeGating(combinator, accept ? { accept } : void 0);
6976
+ } catch {
6977
+ return void 0;
6978
+ }
6979
+ const lines = formatGatingWarnings(report);
6980
+ if (lines.length > 0) {
6981
+ if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
6982
+ ${lines.join("\n")}`);
6983
+ for (const l of lines) console.warn(l);
6984
+ }
6985
+ return report;
6986
+ }
6467
6987
  function compile(combinator, mapFnSources, opts) {
6988
+ const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
6468
6989
  markUnusedValues(combinator);
6469
6990
  const grammarTrivia = combinator._meta.grammarTrivia;
6991
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
6470
6992
  const ctx = {
6471
6993
  vars: 0,
6472
6994
  indent: 1,
@@ -6488,7 +7010,8 @@ function compile(combinator, mapFnSources, opts) {
6488
7010
  recovery: opts?.recovery ?? false,
6489
7011
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
6490
7012
  lazyUsage: analyzeLazyUsage(combinator),
6491
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7013
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7014
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6492
7015
  };
6493
7016
  const r = emit(combinator, ctx, "_pos");
6494
7017
  const coverageRootRuleId = ctx.coverage?.plan.rules.get(combinator);
@@ -6552,6 +7075,7 @@ function compile(combinator, mapFnSources, opts) {
6552
7075
  source,
6553
7076
  inlineExpression,
6554
7077
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
7078
+ ...gatingReport === void 0 ? {} : { gating: gatingReport },
6555
7079
  parse(input, pos = 0) {
6556
7080
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
6557
7081
  },
@@ -6587,6 +7111,7 @@ function compileRuleMap(ruleMap, opts) {
6587
7111
  for (const [, rule] of ruleMap) markUnusedValues(rule);
6588
7112
  const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
6589
7113
  const grammarTrivia = opts?.trivia;
7114
+ const grammarScanSkip = opts?.scanSkip ?? ruleMap.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6590
7115
  const ctx = {
6591
7116
  vars: 0,
6592
7117
  indent: 1,
@@ -6608,7 +7133,8 @@ function compileRuleMap(ruleMap, opts) {
6608
7133
  recovery: opts?.recovery ?? false,
6609
7134
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(ruleMap.map(([, rule]) => rule), coverageWinners) } } : {},
6610
7135
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6611
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7136
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7137
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6612
7138
  };
6613
7139
  const seenNames = /* @__PURE__ */ new Map();
6614
7140
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6622,9 +7148,14 @@ function compileRuleMap(ruleMap, opts) {
6622
7148
  ctx.ruleNames = ruleNames;
6623
7149
  const perEntry = ruleMap.map(([key, rule]) => {
6624
7150
  const savedTrivia = ctx.activeTrivia;
6625
- if (rule._meta.isTrivia) ctx.activeTrivia = void 0;
7151
+ const savedScanSkip = ctx.activeScanSkip;
7152
+ if (rule._meta.isTrivia) {
7153
+ ctx.activeTrivia = void 0;
7154
+ ctx.activeScanSkip = void 0;
7155
+ }
6626
7156
  const r = emit(rule, ctx, "_pos");
6627
7157
  ctx.activeTrivia = savedTrivia;
7158
+ ctx.activeScanSkip = savedScanSkip;
6628
7159
  return { key, rule, r };
6629
7160
  });
6630
7161
  const derivedSrcs = ctx.mapFnSrcs.length === ctx.mapFns.length && ctx.mapFnSrcs.every((s) => s !== null) ? ctx.mapFnSrcs : void 0;
@@ -6697,6 +7228,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6697
7228
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
6698
7229
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
6699
7230
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7231
+ const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6700
7232
  const ruleMap = ruleMapArg.filter(([, val]) => {
6701
7233
  const d = val._def;
6702
7234
  if (d.tag !== "lazy") return true;
@@ -6734,7 +7266,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
6734
7266
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6735
7267
  ns,
6736
7268
  deferFirstSetRefs: true,
6737
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7269
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7270
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6738
7271
  };
6739
7272
  const seen = /* @__PURE__ */ new Map();
6740
7273
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6798,9 +7331,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
6798
7331
  ctx.failLabel = "_pfail";
6799
7332
  ctx.recordFail = true;
6800
7333
  const savedTrivia = ctx.activeTrivia;
6801
- if (rule._meta.isTrivia) ctx.activeTrivia = void 0;
7334
+ const savedScanSkip = ctx.activeScanSkip;
7335
+ if (rule._meta.isTrivia) {
7336
+ ctx.activeTrivia = void 0;
7337
+ ctx.activeScanSkip = void 0;
7338
+ }
6802
7339
  const body = emit(resolved, ctx, "_pos");
6803
7340
  ctx.activeTrivia = savedTrivia;
7341
+ ctx.activeScanSkip = savedScanSkip;
6804
7342
  ctx.indent = savedIndent;
6805
7343
  ctx.failLabel = savedFail;
6806
7344
  ctx.recordFail = savedRec;
@@ -6847,6 +7385,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6847
7385
  }
6848
7386
  const firstSets = /* @__PURE__ */ new Map();
6849
7387
  const firstSetRecipes = /* @__PURE__ */ new Map();
7388
+ const nullable2 = /* @__PURE__ */ new Map();
6850
7389
  for (const [key, rule] of ruleMap) {
6851
7390
  let resolved = rule;
6852
7391
  const d = rule._def;
@@ -6858,15 +7397,18 @@ function compileLinkable(ruleMapArg, ns, opts) {
6858
7397
  }
6859
7398
  }
6860
7399
  firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
6861
- firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { concrete: any(), refs: [] } : leadingFirstSetRecipe(resolved));
7400
+ nullable2.set(key, matchesEmpty(resolved));
7401
+ firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { alts: [[{ set: any(), nullable: false }]] } : leadingFirstSetRecipe(resolved));
6862
7402
  }
6863
7403
  return {
7404
+ v: PARSEMAN_VERSION,
6864
7405
  ns,
6865
7406
  keys: perEntry.map((e) => e.key),
6866
7407
  prelude,
6867
7408
  ruleFns,
6868
7409
  wrappers,
6869
7410
  firstSets,
7411
+ nullable: nullable2,
6870
7412
  firstSetRecipes,
6871
7413
  deps: ruleDependencies(ruleMap),
6872
7414
  needsEmptyTl: !!ctx.needsEmptyTl,
@@ -6945,101 +7487,6 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
6945
7487
  ].join("\n");
6946
7488
  }
6947
7489
 
6948
- // src/combinators/scanTo.ts
6949
- function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
6950
- const meta = {
6951
- firstSet: any(),
6952
- canMatchNewline: true,
6953
- isTrivia: false
6954
- };
6955
- return {
6956
- _tag: "scanTo",
6957
- _meta: meta,
6958
- _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
6959
- parse(input, pos, ctx) {
6960
- let cur = pos;
6961
- const probeCtx = {
6962
- trackLines: ctx.trackLines,
6963
- state: ctx.state,
6964
- ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
6965
- };
6966
- const emit2 = (end) => {
6967
- if (end > pos && cstCaptureActive(ctx)) {
6968
- const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
6969
- pushCstLeaf(ctx, leaf2);
6970
- }
6971
- };
6972
- while (cur < input.length) {
6973
- const s = sentinel.parse(input, cur, probeCtx);
6974
- if (s.ok) {
6975
- emit2(cur);
6976
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6977
- }
6978
- let advanced = false;
6979
- for (const skipper of skip2) {
6980
- const r = skipper.parse(input, cur, probeCtx);
6981
- if (r.ok && r.span.end > cur) {
6982
- cur = r.span.end;
6983
- advanced = true;
6984
- break;
6985
- }
6986
- }
6987
- if (!advanced) cur++;
6988
- }
6989
- if (orEOF) {
6990
- emit2(cur);
6991
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6992
- }
6993
- const sentDef = sentinel._def;
6994
- const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
6995
- return { ok: false, expected, span: { start: pos, end: cur } };
6996
- }
6997
- };
6998
- }
6999
- function balanced(open, close, options = {}) {
7000
- const self = ref();
7001
- const skips = options.skip ?? [];
7002
- const stop = /* @__PURE__ */ new Set([open, close]);
7003
- let bounded = true;
7004
- for (const sk of skips) {
7005
- const cs = firstSetClassChars(sk);
7006
- if (cs === null) {
7007
- bounded = false;
7008
- break;
7009
- }
7010
- for (const ch of cs) stop.add(ch);
7011
- }
7012
- const cls = [...stop].map(escapeClassChar).join("");
7013
- const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
7014
- const inner = many(choice(self, ...skips, content));
7015
- const combi = transform(
7016
- sequence(literal(open), inner, expect(literal(close))),
7017
- // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
7018
- // ParseError (recovered close). `c` is the close string or a ParseError.
7019
- ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
7020
- );
7021
- if (combi._def.tag === "transform") {
7022
- combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
7023
- combi._def.recognitionOnly = true;
7024
- }
7025
- self.define(combi);
7026
- return combi;
7027
- }
7028
- function firstSetClassChars(c) {
7029
- const fs2 = c._meta.firstSet;
7030
- if (fs2.kind === "empty") return [];
7031
- if (fs2.kind !== "ranges") return null;
7032
- const out = [];
7033
- for (const { lo, hi } of fs2.ranges) {
7034
- if (hi - lo > 8) return null;
7035
- for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
7036
- }
7037
- return out;
7038
- }
7039
- function escapeClassChar(ch) {
7040
- return ch.replace(/[\\\]^-]/g, "\\$&");
7041
- }
7042
-
7043
7490
  // src/combinators/withCtx.ts
7044
7491
  function withCtx(extra, combinator) {
7045
7492
  const meta = {
@@ -7096,7 +7543,7 @@ function childrenOf2(def) {
7096
7543
  return [];
7097
7544
  }
7098
7545
  }
7099
- var ruleNameOf = (c) => c._ruleName;
7546
+ var ruleNameOf2 = (c) => c._ruleName;
7100
7547
  function lazyTarget(c) {
7101
7548
  if (c._def.tag !== "lazy") return null;
7102
7549
  try {
@@ -7107,9 +7554,9 @@ function lazyTarget(c) {
7107
7554
  }
7108
7555
  var Unserializable = class extends Error {
7109
7556
  };
7110
- function serializeRuleMap(ruleMap) {
7557
+ function serializeRuleMap(ruleMap, scanSkip) {
7111
7558
  try {
7112
- return new Serializer(ruleMap).run();
7559
+ return new Serializer(ruleMap, scanSkip ?? []).run();
7113
7560
  } catch (e) {
7114
7561
  if (e instanceof Unserializable) {
7115
7562
  if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${e.message}`);
@@ -7163,15 +7610,15 @@ function evalRuleMapIR(ir) {
7163
7610
  if (Array.isArray(it)) {
7164
7611
  const [src, comb] = it;
7165
7612
  gateSrcs.push(src);
7166
- let gate;
7613
+ let gate2;
7167
7614
  try {
7168
- gate = (0, eval)(`(${src})`);
7615
+ gate2 = (0, eval)(`(${src})`);
7169
7616
  } catch {
7170
- gate = () => {
7617
+ gate2 = () => {
7171
7618
  throw new Error("IR gate fn not materialized");
7172
7619
  };
7173
7620
  }
7174
- return { gate, combinator: comb };
7621
+ return { gate: gate2, combinator: comb };
7175
7622
  }
7176
7623
  gateSrcs.push(null);
7177
7624
  return it;
@@ -7270,40 +7717,75 @@ var Serializer = class {
7270
7717
  decls = [];
7271
7718
  ruleValues = /* @__PURE__ */ new Set();
7272
7719
  ruleMap;
7273
- constructor(ruleMap) {
7720
+ scanSkip;
7721
+ constructor(ruleMap, scanSkip = []) {
7274
7722
  this.ruleMap = ruleMap;
7723
+ this.scanSkip = scanSkip;
7275
7724
  for (const [, c] of ruleMap) this.ruleValues.add(this.body(c));
7276
7725
  }
7726
+ /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
7727
+ * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
7728
+ * serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
7729
+ * outside the `(g) => …` factory where `g` is bound. A unit that does reference a
7730
+ * rule can't be carried in options; bail to the full-pieces fallback. */
7731
+ assertNoRuleRef(c, seen) {
7732
+ if (seen.has(c)) return;
7733
+ seen.add(c);
7734
+ if (c._def.tag === "lazy") {
7735
+ if (ruleNameOf2(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
7736
+ const target = lazyTarget(c);
7737
+ if (target) this.assertNoRuleRef(target, seen);
7738
+ return;
7739
+ }
7740
+ for (const child of childrenOf2(c._def)) this.assertNoRuleRef(child, seen);
7741
+ }
7277
7742
  /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
7278
7743
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7279
7744
  * `g[name]` ref — only the top-level entry is unwrapped. */
7280
7745
  body(c) {
7281
- return c._def.tag === "lazy" && ruleNameOf(c) !== void 0 ? lazyTarget(c) ?? c : c;
7746
+ return c._def.tag === "lazy" && ruleNameOf2(c) !== void 0 ? lazyTarget(c) ?? c : c;
7282
7747
  }
7283
7748
  run() {
7284
7749
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
7750
+ for (const c of this.scanSkip) {
7751
+ this.assertNoRuleRef(c, /* @__PURE__ */ new Set());
7752
+ this.analyze(c, /* @__PURE__ */ new Set());
7753
+ }
7285
7754
  let n = 0;
7286
7755
  for (const [c, count] of this.counts) {
7287
7756
  if (this.ruleValues.has(c)) continue;
7288
7757
  if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`);
7289
7758
  }
7759
+ const outerDecls = [];
7760
+ const innerDecls = [];
7761
+ let optionsArg = "";
7762
+ if (this.scanSkip.length > 0) {
7763
+ this.decls = outerDecls;
7764
+ const units = this.scanSkip.map((c) => this.ref(c, null));
7765
+ optionsArg = `{ scanSkip: [${units.join(", ")}] }, `;
7766
+ }
7767
+ this.decls = innerDecls;
7290
7768
  const body = this.ruleMap.map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c))}`).join(",\n");
7291
- if (this.decls.length === 0) return `rules((g) => ({
7769
+ const factory = innerDecls.length === 0 ? `(g) => ({
7292
7770
  ${body}
7293
- }))`;
7294
- return `rules((g) => {
7295
- ${this.decls.map((d) => " " + d).join("\n")}
7771
+ })` : `(g) => {
7772
+ ${innerDecls.map((d) => " " + d).join("\n")}
7296
7773
  return ({
7297
7774
  ${body}
7298
7775
  })
7299
- })`;
7776
+ }`;
7777
+ const call = `rules(${optionsArg}${factory})`;
7778
+ return outerDecls.length === 0 ? call : `(() => {
7779
+ ${outerDecls.map((d) => " " + d).join("\n")}
7780
+ return ${call}
7781
+ })()`;
7300
7782
  }
7301
7783
  /** Count identity references and flag self-referential subtrees. `active` is the
7302
7784
  * set of combinators on the current DFS path (to detect a lazy pointing back). */
7303
7785
  analyze(c, active) {
7304
7786
  const def = c._def;
7305
7787
  if (def.tag === "lazy") {
7306
- if (ruleNameOf(c) !== void 0) return;
7788
+ if (ruleNameOf2(c) !== void 0) return;
7307
7789
  const target = lazyTarget(c);
7308
7790
  if (!target) throw new Unserializable("unresolved unnamed ref");
7309
7791
  if (active.has(target)) {
@@ -7355,7 +7837,7 @@ ${body}
7355
7837
  const kid = (x) => this.ref(x, selfOf);
7356
7838
  switch (def.tag) {
7357
7839
  case "lazy": {
7358
- if (ruleNameOf(c) !== void 0) return `g[${JSON.stringify(ruleNameOf(c))}]`;
7840
+ if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
7359
7841
  const target = lazyTarget(c);
7360
7842
  if (selfOf && target === selfOf.comb) return selfOf.var;
7361
7843
  return this.ref(target, selfOf);
@@ -7409,7 +7891,7 @@ ${body}
7409
7891
  case "skip":
7410
7892
  return `skip(${kid(def.main)}, ${kid(def.skipped)})`;
7411
7893
  case "scanTo":
7412
- return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}], orEOF: ${def.orEOF} })`;
7894
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}]${def.raw ? ", raw: true" : ""}, orEOF: ${def.orEOF} })`;
7413
7895
  case "transform": {
7414
7896
  if (def.fnSrc === void 0) throw new Unserializable("transform without fnSrc");
7415
7897
  return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ", true" : ""})`;
@@ -7490,6 +7972,18 @@ function pickPieces(pieces, names) {
7490
7972
  return pieces.map((pc) => ({ ...pc, keys: pc.keys.filter((k) => keep.has(k) && winner.get(k) === pc), ruleFns: filt(pc.ruleFns, pc), wrappers: filt(pc.wrappers, pc), deps: filt(pc.deps, pc) })).filter((pc) => pc.keys.length > 0);
7491
7973
  }
7492
7974
  function fusedBody(pieces) {
7975
+ for (const p of pieces) {
7976
+ if (p.v === void 0) {
7977
+ throw new Error(
7978
+ `parseman: artifact "${p.ns}" is UNSTAMPED (compiled before the version-lock invariant). Recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse unversioned or cross-version artifacts.`
7979
+ );
7980
+ }
7981
+ if (p.v !== PARSEMAN_VERSION) {
7982
+ throw new Error(
7983
+ `parseman: artifact "${p.ns}" was compiled with parseman ${p.v}, but is being fused with parseman ${PARSEMAN_VERSION}. Compiled grammar artifacts are version-locked \u2014 recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse across versions.`
7984
+ );
7985
+ }
7986
+ }
7493
7987
  const winner = /* @__PURE__ */ new Map();
7494
7988
  for (const p of pieces) for (const k of p.keys) winner.set(k, p);
7495
7989
  for (const [k, p] of winner) {
@@ -7515,23 +8009,38 @@ function fusedBody(pieces) {
7515
8009
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
7516
8010
  const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
7517
8011
  const ANY2 = { kind: "any" };
8012
+ const EMPTY3 = { kind: "empty" };
7518
8013
  const recipes = /* @__PURE__ */ new Map();
7519
8014
  const shallow = /* @__PURE__ */ new Map();
8015
+ const nullable2 = /* @__PURE__ */ new Map();
7520
8016
  for (const [k, p] of winner) {
7521
8017
  const r = p.firstSetRecipes?.get(k);
7522
8018
  if (r) recipes.set(k, r);
7523
8019
  const fs2 = p.firstSets?.get(k);
7524
8020
  if (fs2) shallow.set(k, fs2);
8021
+ const nu = p.nullable?.get(k);
8022
+ if (nu !== void 0) nullable2.set(k, nu);
7525
8023
  }
8024
+ const knownRef = (ref2) => recipes.has(ref2) || shallow.has(ref2);
8025
+ const refFS = (ref2) => knownRef(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2;
8026
+ const refNullable = (ref2) => knownRef(ref2) ? nullable2.get(ref2) ?? true : true;
8027
+ const resolveRecipe = (r) => {
8028
+ let out = EMPTY3;
8029
+ for (const chain of r.alts) {
8030
+ for (const seg of chain) {
8031
+ out = union(out, seg.ref !== void 0 ? refFS(seg.ref) : seg.set);
8032
+ const skippable = seg.ref !== void 0 ? seg.nullable || refNullable(seg.ref) : seg.nullable;
8033
+ if (!skippable) break;
8034
+ }
8035
+ }
8036
+ return out;
8037
+ };
7526
8038
  const finalFS = /* @__PURE__ */ new Map();
7527
- for (const [k] of winner) finalFS.set(k, recipes.get(k)?.concrete ?? shallow.get(k) ?? ANY2);
8039
+ for (const [k] of winner) finalFS.set(k, recipes.has(k) ? EMPTY3 : shallow.get(k) ?? ANY2);
7528
8040
  for (let iter = 0; iter <= recipes.size; iter++) {
7529
8041
  let changed = false;
7530
8042
  for (const [k, r] of recipes) {
7531
- let fs2 = r.concrete;
7532
- for (const ref2 of r.refs) {
7533
- fs2 = union(fs2, recipes.has(ref2) || shallow.has(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2);
7534
- }
8043
+ const fs2 = resolveRecipe(r);
7535
8044
  if (JSON.stringify(finalFS.get(k)) !== JSON.stringify(fs2)) {
7536
8045
  finalFS.set(k, fs2);
7537
8046
  changed = true;
@@ -7579,8 +8088,8 @@ function materializePiece(p, trivia2, captureTerminals = false) {
7579
8088
  return pieces;
7580
8089
  }
7581
8090
 
7582
- // src/combinators/guard.ts
7583
- function guard(predicate) {
8091
+ // src/combinators/gate.ts
8092
+ function gate(predicate) {
7584
8093
  const meta = {
7585
8094
  firstSet: { kind: "any" },
7586
8095
  canMatchNewline: false,
@@ -7837,17 +8346,17 @@ function isGatedArmExpr(e) {
7837
8346
  return hasGate && hasCombinator;
7838
8347
  }
7839
8348
  function gatedArmParts(e) {
7840
- let gate;
8349
+ let gate2;
7841
8350
  let combinator;
7842
8351
  for (const prop of e.properties) {
7843
8352
  if (prop.type !== "Property") return null;
7844
8353
  const op = prop;
7845
8354
  const name = propName(op);
7846
- if (name === "gate") gate = op.value;
8355
+ if (name === "gate") gate2 = op.value;
7847
8356
  else if (name === "combinator") combinator = op.value;
7848
8357
  else return null;
7849
8358
  }
7850
- return gate && combinator ? { gate, combinator } : null;
8359
+ return gate2 && combinator ? { gate: gate2, combinator } : null;
7851
8360
  }
7852
8361
  function isCombinator(v2) {
7853
8362
  return !!v2 && typeof v2 === "object" && "_def" in v2;
@@ -8049,14 +8558,14 @@ function exprToCombi(node2, scope, code, mfs) {
8049
8558
  return null;
8050
8559
  }
8051
8560
  }
8052
- if (callee.name === "guard") {
8561
+ if (callee.name === "gate" || callee.name === "guard") {
8053
8562
  if (code === void 0 || mfs === void 0) return null;
8054
8563
  const [predArg] = node2.arguments;
8055
8564
  if (!predArg || predArg.type === "SpreadElement") return null;
8056
8565
  const predSrc = stripTsFromSource(predArg, code);
8057
8566
  mfs.push(predSrc);
8058
8567
  try {
8059
- const combi = guard(() => true);
8568
+ const combi = gate(() => true);
8060
8569
  if (combi._def.tag !== "guard") return null;
8061
8570
  combi._def.predSrc = predSrc;
8062
8571
  return combi;
@@ -8680,21 +9189,32 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8680
9189
  return null;
8681
9190
  }
8682
9191
  const optionsArg = optionsFirst ? arg0 : arg1;
8683
- const triviaValue = optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
8684
- (p) => p.key?.name === "trivia"
9192
+ const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
9193
+ (p) => p.key?.name === name
8685
9194
  )?.value : void 0;
9195
+ const triviaValue = optionValue("trivia");
8686
9196
  const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
8687
- return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {} };
9197
+ const scanSkipValue = optionValue("scanSkip");
9198
+ const gScanSkip = scanSkipValue ? evaluateCombinatorArray(scanSkipValue, scope, code) ?? void 0 : void 0;
9199
+ if (gScanSkip) {
9200
+ for (const rule of ruleMap.values()) {
9201
+ if (rule && !rule._meta.isTrivia) {
9202
+ ;
9203
+ rule._meta.grammarScanSkip = gScanSkip;
9204
+ }
9205
+ }
9206
+ }
9207
+ return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
8688
9208
  };
8689
9209
  const compileRulesFactory = (init, label2) => {
8690
9210
  const evaluated = evaluateRulesFactory(init, label2);
8691
9211
  if (!evaluated) return null;
8692
- const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, recovery, coverage: grammarCoverage });
9212
+ const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
8693
9213
  if (!compiled) {
8694
9214
  warn(init.start, `${label2}: rule map couldn't be inlined`);
8695
9215
  return null;
8696
9216
  }
8697
- return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {} };
9217
+ return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
8698
9218
  };
8699
9219
  const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
8700
9220
  const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
@@ -8702,13 +9222,14 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8702
9222
  const isPickCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "pick";
8703
9223
  const localRuleMaps = /* @__PURE__ */ new Map();
8704
9224
  const localGrammarTrivia = /* @__PURE__ */ new Map();
9225
+ const localGrammarScanSkip = /* @__PURE__ */ new Map();
8705
9226
  const nsFor = (label2) => `_${createHash("sha1").update(`${id}#${label2}`).digest("hex").slice(0, 8)}_`;
8706
9227
  const stripIndent = (s) => s.replace(/\n[ \t]+/g, "\n");
8707
9228
  const serializePieces = (p) => {
8708
9229
  const mapLit = (m, stripVals = false) => `new Map([${[...m].map(
8709
9230
  ([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(stripVals && typeof v2 === "string" ? stripIndent(v2) : v2)}]`
8710
9231
  ).join(", ")}])`;
8711
- return `{ ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
9232
+ return `{ v: ${JSON.stringify(p.v)}, ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, nullable: ${p.nullable ? mapLit(p.nullable) : "new Map()"}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
8712
9233
  };
8713
9234
  const isSpread = (it) => "__spreadLocal" in it;
8714
9235
  const isIR = (it) => "ir" in it && !("ruleFns" in it);
@@ -8819,12 +9340,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8819
9340
  }
8820
9341
  return pieces;
8821
9342
  };
8822
- const localCarried = (rm, label2, composing) => {
9343
+ const localCarried = (rm, label2, composing, scanSkip) => {
8823
9344
  const entries = [...rm];
8824
9345
  const ns = nsFor(label2);
8825
- const ir = serializeRuleMap(entries);
9346
+ const ir = serializeRuleMap(entries, scanSkip);
8826
9347
  if (ir) return { carried: [{ ns, ir }] };
8827
- const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, recovery });
9348
+ const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
8828
9349
  return p ? { carried: [p] } : null;
8829
9350
  };
8830
9351
  const argPieces = (arg, label2, composing) => {
@@ -8849,18 +9370,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8849
9370
  }
8850
9371
  }
8851
9372
  if (isRulesCall(arg)) {
8852
- const rulesArgs = arg.arguments;
8853
- const a0 = rulesArgs[0];
8854
- const a1 = rulesArgs[1];
8855
- const optionsFirst = a0?.type === "ObjectExpression";
8856
- const factory = optionsFirst ? a1 : a0;
8857
- const rm = factory ? evaluateParserFactory(factory, scope, code, []) : null;
8858
- return rm ? localCarried(rm, label2, composing) : null;
9373
+ const evaluated = evaluateRulesFactory(arg, label2);
9374
+ return evaluated ? localCarried(evaluated.ruleMap, label2, composing, evaluated.scanSkip) : null;
8859
9375
  }
8860
9376
  if (arg.type === "Identifier") {
8861
9377
  const name = arg.name;
8862
9378
  const rm = localRuleMaps.get(name);
8863
- if (rm) return localCarried(rm, label2, composing);
9379
+ if (rm) return localCarried(rm, label2, composing, localGrammarScanSkip.get(name));
8864
9380
  const composed = localComposedCarried.get(name);
8865
9381
  if (composed) return { carried: composed };
8866
9382
  const pieces = importedPieces(name);
@@ -8948,11 +9464,15 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8948
9464
  }
8949
9465
  const localArg = elements[elements.length - 1];
8950
9466
  let localRules = null;
9467
+ let localScanSkip;
8951
9468
  if (isRulesCall(localArg)) {
8952
9469
  const evaluated = evaluateRulesFactory(localArg, `composeLeaf${init.start}`);
8953
9470
  localRules = evaluated?.ruleMap ?? null;
9471
+ localScanSkip = evaluated?.scanSkip;
8954
9472
  } else if (localArg.type === "Identifier") {
8955
- localRules = localRuleMaps.get(localArg.name) ?? null;
9473
+ const name = localArg.name;
9474
+ localRules = localRuleMaps.get(name) ?? null;
9475
+ localScanSkip = localGrammarScanSkip.get(name);
8956
9476
  }
8957
9477
  if (!localRules) {
8958
9478
  warn(init.start, "composeLeaf(): final argument must be a local rules() map");
@@ -8970,7 +9490,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8970
9490
  }
8971
9491
  try {
8972
9492
  const localNs = nsFor(`composeLeaf${init.start}`);
8973
- const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, recovery });
9493
+ const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
8974
9494
  if (!plainLocalPiece) {
8975
9495
  warn(init.start, "composeLeaf(): local rules could not be statically compiled");
8976
9496
  return null;
@@ -9072,15 +9592,19 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9072
9592
  if (!compiledRules) continue;
9073
9593
  localRuleMaps.set(varName, compiledRules.ruleMap);
9074
9594
  if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia);
9595
+ if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip);
9075
9596
  let replacement = withCoverageDefinitions(
9076
9597
  compiledRules.replacement,
9077
9598
  compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(compiledRules.replacement)
9078
9599
  );
9079
9600
  if (exportPrefix) {
9080
9601
  const ns = nsFor(varName);
9081
- const pieces = compileLinkable([...compiledRules.ruleMap], ns, { recovery });
9602
+ const pieces = compileLinkable([...compiledRules.ruleMap], ns, {
9603
+ ...compiledRules.scanSkip ? { scanSkip: compiledRules.scanSkip } : {},
9604
+ recovery
9605
+ });
9082
9606
  if (pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
9083
- const ir = serializeRuleMap([...compiledRules.ruleMap]);
9607
+ const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
9084
9608
  replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
9085
9609
  }
9086
9610
  }
@@ -9200,9 +9724,18 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9200
9724
  ms.overwrite(imp.start, imp.end, stripped);
9201
9725
  }
9202
9726
  }
9203
- for (const { start, end, replacement } of (runtimeComposeFallback ? [] : replacements).sort((a, b) => b.start - a.start)) {
9727
+ const applied = runtimeComposeFallback ? [] : replacements;
9728
+ for (const { start, end, replacement } of applied.slice().sort((a, b) => b.start - a.start)) {
9204
9729
  ms.overwrite(start, end, replacement);
9205
9730
  }
9731
+ if (applied.length > 0) {
9732
+ ms.prepend(
9733
+ `// Generated by parseman v${PARSEMAN_VERSION} \u2014 DO NOT EDIT.
9734
+ // Version-locked: compile AND fuse/link this artifact with parseman v${PARSEMAN_VERSION} ONLY.
9735
+ // Parseman does not read artifacts across versions; recompile if the version differs.
9736
+ `
9737
+ );
9738
+ }
9206
9739
  const unlowered = endLoweringCapture();
9207
9740
  if (warnUnloweredRegex) {
9208
9741
  for (const src of unlowered) {