parseman 0.31.0 → 0.32.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 (40) 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/linker.d.ts.map +1 -1
  18. package/dist/functional/run.d.ts.map +1 -1
  19. package/dist/index.cjs +715 -237
  20. package/dist/index.cjs.map +4 -4
  21. package/dist/index.d.ts +5 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +715 -237
  24. package/dist/index.js.map +4 -4
  25. package/dist/language-service/index.cjs +2 -0
  26. package/dist/language-service/index.cjs.map +2 -2
  27. package/dist/language-service/index.js +2 -0
  28. package/dist/language-service/index.js.map +2 -2
  29. package/dist/plugin/index.cjs +741 -249
  30. package/dist/plugin/index.cjs.map +4 -4
  31. package/dist/plugin/index.d.ts.map +1 -1
  32. package/dist/plugin/index.js +741 -249
  33. package/dist/plugin/index.js.map +4 -4
  34. package/dist/spec/index.cjs.map +1 -1
  35. package/dist/spec/index.js.map +1 -1
  36. package/dist/types.d.ts +18 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/version.d.ts +17 -0
  39. package/dist/version.d.ts.map +1 -0
  40. package/package.json +2 -1
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  absoluteSpanCST: () => absoluteSpanCST,
27
27
  absolutize: () => absolutize,
28
28
  absolutizeCST: () => absolutizeCST,
29
+ analyzeGating: () => analyzeGating,
29
30
  annotateSpan: () => annotateSpan,
30
31
  applyEdit: () => applyEdit,
31
32
  attempt: () => attempt,
@@ -50,8 +51,11 @@ __export(index_exports, {
50
51
  cstBuildHost: () => cstBuildHost,
51
52
  expect: () => expect,
52
53
  field: () => field,
54
+ firstSetToString: () => firstSetToString,
55
+ formatGatingWarnings: () => formatGatingWarnings,
53
56
  gapIsSignificant: () => gapIsSignificant,
54
57
  gapText: () => gapText,
58
+ gate: () => gate,
55
59
  grammarCoverageDefinitions: () => grammarCoverageDefinitions,
56
60
  guard: () => guard,
57
61
  indentMixed: () => indentMixed,
@@ -186,10 +190,13 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
186
190
  return true;
187
191
  }
188
192
  }
193
+ function isZeroWidthAssertion(p) {
194
+ return p._def.tag === "not";
195
+ }
189
196
  function sequenceFirstSet(parsers) {
190
197
  let fs = empty();
191
198
  for (const p of parsers) {
192
- fs = union(fs, p._meta.firstSet);
199
+ if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet);
193
200
  if (!matchesEmpty(p)) return fs;
194
201
  }
195
202
  return fs;
@@ -219,7 +226,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
219
226
  case "sequence": {
220
227
  let out = empty();
221
228
  for (const term of d.parsers) {
222
- out = union(out, fs(term));
229
+ if (!isZeroWidthAssertion(term)) out = union(out, fs(term));
223
230
  if (!matchesEmpty(term)) return out;
224
231
  }
225
232
  return out;
@@ -507,9 +514,9 @@ var SPACE_RANGES = [
507
514
  [12288, 12288],
508
515
  [65279, 65279]
509
516
  ];
510
- function shorthandRanges(ch) {
511
- if (ch === "d") return [[48, 57]];
512
- if (ch === "s") return SPACE_RANGES;
517
+ function shorthandRanges(ch2) {
518
+ if (ch2 === "d") return [[48, 57]];
519
+ if (ch2 === "s") return SPACE_RANGES;
513
520
  return [[48, 57], [65, 90], [97, 122], [95, 95]];
514
521
  }
515
522
  function readUnicodeEscape(body, i) {
@@ -522,9 +529,9 @@ function parseClassRanges(body) {
522
529
  const ranges = [];
523
530
  let i = 0;
524
531
  const readAtom = () => {
525
- const ch = body[i];
526
- if (ch === void 0) return null;
527
- if (ch === "\\") {
532
+ const ch2 = body[i];
533
+ if (ch2 === void 0) return null;
534
+ if (ch2 === "\\") {
528
535
  const uni = readUnicodeEscape(body, i);
529
536
  if (uni) {
530
537
  i = uni.next;
@@ -538,8 +545,8 @@ function parseClassRanges(body) {
538
545
  if (e >= "a" && e <= "z" || e >= "A" && e <= "Z") return null;
539
546
  return { cp: e.codePointAt(0) };
540
547
  }
541
- i += ch.length;
542
- return { cp: ch.codePointAt(0) };
548
+ i += ch2.length;
549
+ return { cp: ch2.codePointAt(0) };
543
550
  };
544
551
  while (i < body.length) {
545
552
  const lo = readAtom();
@@ -606,12 +613,35 @@ function shortScanner(source, flags) {
606
613
  return minOne && end === pos ? null : end;
607
614
  };
608
615
  }
616
+ function asciiCaseFold(fs) {
617
+ if (fs.kind !== "ranges") return fs;
618
+ const ranges = fs.ranges.map((r) => ({ lo: r.lo, hi: r.hi }));
619
+ const add = (lo, hi) => {
620
+ if (lo <= hi) ranges.push({ lo, hi });
621
+ };
622
+ for (const r of fs.ranges) {
623
+ const uLo = Math.max(r.lo, 65), uHi = Math.min(r.hi, 90);
624
+ add(uLo + 32, uHi + 32);
625
+ const lLo = Math.max(r.lo, 97), lHi = Math.min(r.hi, 122);
626
+ add(lLo - 32, lHi - 32);
627
+ }
628
+ ranges.sort((a, b) => a.lo - b.lo);
629
+ const merged = [];
630
+ for (const r of ranges) {
631
+ const last = merged[merged.length - 1];
632
+ if (last && r.lo <= last.hi + 1) last.hi = Math.max(last.hi, r.hi);
633
+ else merged.push({ lo: r.lo, hi: r.hi });
634
+ }
635
+ return { kind: "ranges", ranges: merged };
636
+ }
609
637
  function regex(pattern, flags = "") {
610
638
  const source = typeof pattern === "string" ? pattern : pattern.source;
611
639
  const resolvedFlags = typeof pattern === "string" ? flags : pattern.flags;
612
640
  const anchored = new RegExp(source, "y" + resolvedFlags.replace(/[gy]/g, ""));
613
641
  const scan = shortScanner(source, resolvedFlags);
614
- const { firstSet: firstSet2, canMatchNewline } = (firstSetAnalyzer ?? permissiveFirstSet)(source);
642
+ const raw = (firstSetAnalyzer ?? permissiveFirstSet)(source);
643
+ const firstSet2 = !resolvedFlags.includes("i") ? raw.firstSet : resolvedFlags.includes("u") || resolvedFlags.includes("v") ? any() : asciiCaseFold(raw.firstSet);
644
+ const canMatchNewline = raw.canMatchNewline;
615
645
  const meta = { firstSet: firstSet2, canMatchNewline, isTrivia: false };
616
646
  return {
617
647
  _tag: "regex",
@@ -666,8 +696,8 @@ function parseRegex(src) {
666
696
  const parseSeq = () => {
667
697
  const parts = [];
668
698
  while (i < src.length) {
669
- const ch = src[i];
670
- if (ch === "|" || ch === ")") break;
699
+ const ch2 = src[i];
700
+ if (ch2 === "|" || ch2 === ")") break;
671
701
  parts.push(parseQuantified());
672
702
  }
673
703
  if (parts.length === 0) return EMPTY2;
@@ -675,18 +705,18 @@ function parseRegex(src) {
675
705
  };
676
706
  const parseQuantified = () => {
677
707
  const atom = parseAtom();
678
- const ch = src[i];
708
+ const ch2 = src[i];
679
709
  let min = null;
680
- if (ch === "*") {
710
+ if (ch2 === "*") {
681
711
  min = 0;
682
712
  i++;
683
- } else if (ch === "+") {
713
+ } else if (ch2 === "+") {
684
714
  min = 1;
685
715
  i++;
686
- } else if (ch === "?") {
716
+ } else if (ch2 === "?") {
687
717
  min = 0;
688
718
  i++;
689
- } else if (ch === "{") {
719
+ } else if (ch2 === "{") {
690
720
  min = parseBraceQuantifier();
691
721
  }
692
722
  if (min === null) return atom;
@@ -702,8 +732,8 @@ function parseRegex(src) {
702
732
  return Number.parseInt(m[1], 10);
703
733
  };
704
734
  const parseAtom = () => {
705
- const ch = src[i];
706
- switch (ch) {
735
+ const ch2 = src[i];
736
+ switch (ch2) {
707
737
  case "(":
708
738
  return parseGroup();
709
739
  case "[":
@@ -724,8 +754,8 @@ function parseRegex(src) {
724
754
  return charNode(fromRange(123, 123), false);
725
755
  // a `{` with nothing to quantify is a literal brace
726
756
  default:
727
- i += ch.length;
728
- return charNode(fromRange(ch.codePointAt(0), ch.codePointAt(0)), ch === "\n");
757
+ i += ch2.length;
758
+ return charNode(fromRange(ch2.codePointAt(0), ch2.codePointAt(0)), ch2 === "\n");
729
759
  }
730
760
  };
731
761
  const parseGroup = () => {
@@ -1165,8 +1195,8 @@ function choice(...args) {
1165
1195
  }
1166
1196
  }
1167
1197
  if (idx >= 0) {
1168
- const gate = gates[idx];
1169
- if (gate && !gate(ctx.state)) {
1198
+ const gate2 = gates[idx];
1199
+ if (gate2 && !gate2(ctx.state)) {
1170
1200
  return { ok: false, expected: deriveExpected(parsers[idx]), span: { start: pos, end: pos } };
1171
1201
  }
1172
1202
  const result = parsers[idx].parse(input, pos, ctx);
@@ -1512,11 +1542,11 @@ function tryFastLabeledScan(input, cur, trivia2) {
1512
1542
  function recordTriviaChunks(ctx, chunks) {
1513
1543
  const kinds = ctx.triviaKindLabels;
1514
1544
  const mask = ctx._triviaCaptureMask;
1515
- for (const ch of chunks) {
1516
- pushTriviaLogEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1545
+ for (const ch2 of chunks) {
1546
+ pushTriviaLogEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1517
1547
  if (ctx.captureTrivia && (ctx._cstBuf !== void 0 || ctx._cstTriviaLog !== void 0)) {
1518
- if (mask === void 0 || kinds === void 0 || (mask & 1 << ch.kindIndex) !== 0) {
1519
- pushCstTriviaEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1548
+ if (mask === void 0 || kinds === void 0 || (mask & 1 << ch2.kindIndex) !== 0) {
1549
+ pushCstTriviaEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1520
1550
  }
1521
1551
  }
1522
1552
  }
@@ -2292,6 +2322,12 @@ function rules(a, b) {
2292
2322
  if (rule && !rule._meta.isTrivia) rule._meta.grammarTrivia = options.trivia;
2293
2323
  }
2294
2324
  }
2325
+ if (options?.scanSkip != null) {
2326
+ for (const key of Object.keys(definitions)) {
2327
+ const rule = cache[key];
2328
+ if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
2329
+ }
2330
+ }
2295
2331
  for (const key of Object.keys(definitions)) {
2296
2332
  markUnusedValues(cache[key]);
2297
2333
  }
@@ -2774,9 +2810,11 @@ function parse(combinator, input, opts = {}) {
2774
2810
  const _errors = opts.recover ? [] : void 0;
2775
2811
  const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
2776
2812
  const grammarTrivia = combinator._meta.grammarTrivia;
2813
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
2777
2814
  const ctx = {
2778
2815
  trackLines,
2779
2816
  ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...grammarTrivia._meta.triviaKindLabels ? { triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {} } : {},
2817
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
2780
2818
  ..._errors !== void 0 ? { _errors } : {},
2781
2819
  ..._probe !== void 0 ? { _probe } : {}
2782
2820
  };
@@ -2885,6 +2923,413 @@ function leaf(root, fn) {
2885
2923
  };
2886
2924
  }
2887
2925
 
2926
+ // src/version.ts
2927
+ var PARSEMAN_VERSION = "0.32.0";
2928
+
2929
+ // src/analysis/gating.ts
2930
+ var isAny = (fs) => fs.kind === "any";
2931
+ function intersects2(a, b) {
2932
+ if (a.kind === "any" || b.kind === "any") return true;
2933
+ if (a.kind === "empty" || b.kind === "empty") return false;
2934
+ for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
2935
+ return false;
2936
+ }
2937
+ function intersection(a, b) {
2938
+ if (a.kind === "any") return b;
2939
+ if (b.kind === "any") return a;
2940
+ if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
2941
+ const ranges = [];
2942
+ for (const ra of a.ranges) for (const rb of b.ranges) {
2943
+ const lo = Math.max(ra.lo, rb.lo);
2944
+ const hi = Math.min(ra.hi, rb.hi);
2945
+ if (lo <= hi) ranges.push({ lo, hi });
2946
+ }
2947
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
2948
+ }
2949
+ function combine(sets) {
2950
+ let acc = { kind: "empty" };
2951
+ for (const s of sets) {
2952
+ if (s.kind === "any") return { kind: "any" };
2953
+ if (s.kind === "empty") continue;
2954
+ acc = acc.kind === "ranges" ? { kind: "ranges", ranges: [...acc.ranges, ...s.ranges] } : s;
2955
+ }
2956
+ return acc;
2957
+ }
2958
+ var ch = (c) => c >= 32 && c < 127 ? `'${String.fromCharCode(c)}'` : `\\u${c}`;
2959
+ function firstSetToString(fs) {
2960
+ if (fs.kind === "any") return "ANY";
2961
+ if (fs.kind === "empty") return "(empty)";
2962
+ return fs.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
2963
+ }
2964
+ var ruleNameOf = (p) => p._ruleName;
2965
+ var SUGGESTIONS = {
2966
+ "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.",
2967
+ "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.",
2968
+ "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.",
2969
+ "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.",
2970
+ "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2971
+ "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2972
+ };
2973
+ function classifyBroadArm(arm) {
2974
+ const seen = /* @__PURE__ */ new Set();
2975
+ const walk2 = (p) => {
2976
+ if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2977
+ seen.add(p);
2978
+ const d = p._def;
2979
+ switch (d.tag) {
2980
+ case "literal":
2981
+ case "regex":
2982
+ case "keywords":
2983
+ return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2984
+ case "not":
2985
+ return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
2986
+ case "scanTo":
2987
+ return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2988
+ // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
2989
+ // its own — it is the poison. `withCtx`/`recover` FORWARD their inner
2990
+ // first-set (see firstSetOf), so a broad result comes from the inner parser:
2991
+ // walk into it for a precise cause.
2992
+ case "guard":
2993
+ return { cause: "opaque-wrapper", detail: `opaque wrapper (${d.tag})` };
2994
+ // attempt/withCtx/recover FORWARD their inner first-set (transparent for
2995
+ // dispatch), so a broad result comes from the inner parser — walk into it.
2996
+ case "attempt":
2997
+ case "withCtx":
2998
+ case "recover":
2999
+ return walk2(d.parser);
3000
+ case "lazy": {
3001
+ const name = p._ruleName;
3002
+ try {
3003
+ const inner = walk2(d.thunk());
3004
+ if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
3005
+ return inner;
3006
+ } catch {
3007
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3008
+ }
3009
+ }
3010
+ case "optional":
3011
+ case "many": {
3012
+ const inner = walk2(d.parser);
3013
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3014
+ }
3015
+ case "oneOrMore":
3016
+ case "transform":
3017
+ case "label":
3018
+ case "field":
3019
+ case "trivia":
3020
+ case "token":
3021
+ case "leaf":
3022
+ case "node":
3023
+ case "grammar":
3024
+ case "expect":
3025
+ return walk2(d.parser);
3026
+ case "skip":
3027
+ return walk2(d.main);
3028
+ case "sequence": {
3029
+ let sawNullablePrefix = false;
3030
+ for (const t of d.parsers) {
3031
+ if (t._def.tag === "not") {
3032
+ sawNullablePrefix = true;
3033
+ continue;
3034
+ }
3035
+ if (isAny(firstSetOf(t))) {
3036
+ const inner = walk2(t);
3037
+ if (sawNullablePrefix)
3038
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3039
+ return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
3040
+ }
3041
+ if (matchesEmpty(t)) {
3042
+ sawNullablePrefix = true;
3043
+ continue;
3044
+ }
3045
+ return null;
3046
+ }
3047
+ return { cause: "broad-recognizer", detail: "sequence of only nullable/zero-width terms" };
3048
+ }
3049
+ case "choice": {
3050
+ for (const a of d.parsers) {
3051
+ const r = walk2(a);
3052
+ if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3053
+ }
3054
+ return null;
3055
+ }
3056
+ default:
3057
+ return { cause: "broad-recognizer", detail: `unmodeled construct (${d.tag})` };
3058
+ }
3059
+ };
3060
+ return walk2(arm) ?? { cause: "broad-recognizer", detail: "broad (cause not localized)" };
3061
+ }
3062
+ function peelToLeading(arm) {
3063
+ let d = arm._def;
3064
+ let cur = arm;
3065
+ for (; ; ) {
3066
+ if (d.tag === "node" || d.tag === "grammar" || d.tag === "transform" || d.tag === "label" || d.tag === "attempt") {
3067
+ cur = d.parser;
3068
+ d = cur._def;
3069
+ continue;
3070
+ }
3071
+ break;
3072
+ }
3073
+ if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
3074
+ return cur;
3075
+ }
3076
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
3077
+ function detectAntiPatterns(rule, arms) {
3078
+ const out = [];
3079
+ arms.forEach((arm, i) => {
3080
+ const lead = peelToLeading(arm);
3081
+ const ld = lead._def;
3082
+ if (ld.tag === "not") {
3083
+ const inner = ld.parser;
3084
+ if (inner._def.tag === "not") {
3085
+ out.push({
3086
+ kind: "double-not",
3087
+ rule,
3088
+ armIndex: i,
3089
+ 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."
3090
+ });
3091
+ } else {
3092
+ out.push({
3093
+ kind: "leading-not",
3094
+ rule,
3095
+ armIndex: i,
3096
+ 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."
3097
+ });
3098
+ }
3099
+ }
3100
+ if (ld.tag === "regex" && KEYWORD_REGEX_RE.test(ld.source)) {
3101
+ out.push({
3102
+ kind: "keyword-regex",
3103
+ rule,
3104
+ armIndex: i,
3105
+ 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).`
3106
+ });
3107
+ }
3108
+ });
3109
+ return out;
3110
+ }
3111
+ function analyzeGating(entry, opts) {
3112
+ const raw = [];
3113
+ const antiPatterns = [];
3114
+ const visited = /* @__PURE__ */ new Set();
3115
+ const visit = (p, enclosingRule) => {
3116
+ if (visited.has(p)) return;
3117
+ visited.add(p);
3118
+ const d = p._def;
3119
+ const rule = ruleNameOf(p) ?? enclosingRule;
3120
+ if (d.tag === "choice") {
3121
+ raw.push({ g: analyzeChoice(p, d, rule), rule });
3122
+ antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3123
+ }
3124
+ const rec = d;
3125
+ const kids = [];
3126
+ if (Array.isArray(rec.parsers)) kids.push(...rec.parsers);
3127
+ for (const k of ["parser", "main", "skipped", "separator", "sentinel"])
3128
+ if (rec[k]) kids.push(rec[k]);
3129
+ if (Array.isArray(rec.skip)) kids.push(...rec.skip);
3130
+ if (d.tag === "lazy") {
3131
+ try {
3132
+ kids.push(d.thunk());
3133
+ } catch {
3134
+ }
3135
+ }
3136
+ for (const k of kids) visit(k, rule);
3137
+ };
3138
+ visit(entry, ruleNameOf(entry) ?? "<entry>");
3139
+ const perRule = /* @__PURE__ */ new Map();
3140
+ for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3141
+ const seenInRule = /* @__PURE__ */ new Map();
3142
+ const accept = new Set(opts?.accept ?? []);
3143
+ const usedAccept = /* @__PURE__ */ new Set();
3144
+ const choices = raw.map(({ g, rule }) => {
3145
+ const n = seenInRule.get(rule) ?? 0;
3146
+ seenInRule.set(rule, n + 1);
3147
+ const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
3148
+ const isAccepted = g.gates === "no" && accept.has(id);
3149
+ if (isAccepted) usedAccept.add(id);
3150
+ return { ...g, id, accepted: isAccepted };
3151
+ });
3152
+ const gated = choices.filter((c) => c.gates === "yes").length;
3153
+ const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3154
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3155
+ const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3156
+ const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3157
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3158
+ }
3159
+ function analyzeChoice(p, d, rule) {
3160
+ const arms = d.parsers;
3161
+ const shallow = arms.map((a) => a._meta.firstSet);
3162
+ const deep = arms.map((a) => firstSetOf(a));
3163
+ const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3164
+ const { cause, detail } = classifyBroadArm(arms[index]);
3165
+ return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3166
+ });
3167
+ const overlaps = [];
3168
+ for (let i = 0; i < deep.length; i++)
3169
+ for (let j = i + 1; j < deep.length; j++)
3170
+ if (!isAny(deep[i]) && !isAny(deep[j]) && intersects2(deep[i], deep[j]))
3171
+ overlaps.push({
3172
+ a: i,
3173
+ b: j,
3174
+ on: intersection(deep[i], deep[j]),
3175
+ 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."
3176
+ });
3177
+ let gates;
3178
+ if (d.disjoint) gates = "yes";
3179
+ else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3180
+ else gates = "no";
3181
+ return {
3182
+ rule,
3183
+ strategy: d.strategy?.tag ?? "firstMatch",
3184
+ gates,
3185
+ combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3186
+ anyArms,
3187
+ overlaps
3188
+ };
3189
+ }
3190
+ function formatGatingWarnings(report) {
3191
+ const lines = [];
3192
+ for (const c of report.ungated) {
3193
+ lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
3194
+ for (const a of c.anyArms)
3195
+ lines.push(` \xB7 arm[${a.index}] first-set ANY (${a.cause}): ${a.detail}
3196
+ fix: ${a.suggestion}`);
3197
+ for (const o of c.overlaps)
3198
+ lines.push(` \xB7 arm[${o.a}] \u2229 arm[${o.b}] overlap on ${firstSetToString(o.on)}
3199
+ fix: ${o.suggestion}`);
3200
+ lines.push(` (intentional? accept it in the gating snapshot: { accept: ['${c.id}'] }.)`);
3201
+ }
3202
+ for (const ap of report.antiPatterns)
3203
+ lines.push(`parseman anti-pattern [${ap.kind}] @ ${ap.rule} arm[${ap.armIndex}]: ${ap.message}`);
3204
+ return lines;
3205
+ }
3206
+
3207
+ // src/combinators/scanTo.ts
3208
+ function resolveScanSkip(explicitSkip, raw, ctx) {
3209
+ if (raw) return [];
3210
+ const trivia2 = ctx.trivia;
3211
+ const ambient = ctx.scanSkip;
3212
+ if (!trivia2 && !ambient) return explicitSkip;
3213
+ const out = [];
3214
+ if (trivia2) out.push(trivia2);
3215
+ if (ambient) out.push(...ambient);
3216
+ out.push(...explicitSkip);
3217
+ return out;
3218
+ }
3219
+ function scanTo(sentinel, { skip: skip2, raw = false, orEOF = false } = {}) {
3220
+ const meta = {
3221
+ firstSet: any(),
3222
+ canMatchNewline: true,
3223
+ isTrivia: false
3224
+ };
3225
+ const explicitSkip = skip2 ?? [];
3226
+ return {
3227
+ _tag: "scanTo",
3228
+ _meta: meta,
3229
+ _def: { tag: "scanTo", sentinel, skip: explicitSkip, raw, orEOF },
3230
+ parse(input, pos, ctx) {
3231
+ let cur = pos;
3232
+ const skip3 = resolveScanSkip(explicitSkip, raw, ctx);
3233
+ const probeCtx = {
3234
+ trackLines: ctx.trackLines,
3235
+ state: ctx.state,
3236
+ ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
3237
+ };
3238
+ const emit2 = (end) => {
3239
+ if (end > pos && cstCaptureActive(ctx)) {
3240
+ const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
3241
+ pushCstLeaf(ctx, leaf2);
3242
+ }
3243
+ };
3244
+ while (cur < input.length) {
3245
+ const s = sentinel.parse(input, cur, probeCtx);
3246
+ if (s.ok) {
3247
+ emit2(cur);
3248
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3249
+ }
3250
+ let advanced = false;
3251
+ for (const skipper of skip3) {
3252
+ const r = skipper.parse(input, cur, probeCtx);
3253
+ if (r.ok && r.span.end > cur) {
3254
+ cur = r.span.end;
3255
+ advanced = true;
3256
+ break;
3257
+ }
3258
+ }
3259
+ if (!advanced) cur++;
3260
+ }
3261
+ if (orEOF) {
3262
+ emit2(cur);
3263
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3264
+ }
3265
+ const sentDef = sentinel._def;
3266
+ const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
3267
+ return { ok: false, expected, span: { start: pos, end: cur } };
3268
+ }
3269
+ };
3270
+ }
3271
+ function balanced(open, close, options = {}) {
3272
+ const ownSkip = options.skip ?? [];
3273
+ const combi = buildBalancedInterior(open, close, ownSkip);
3274
+ if (options.raw) return combi;
3275
+ const eagerParse = combi.parse.bind(combi);
3276
+ const cache = /* @__PURE__ */ new Map();
3277
+ combi._balancedAmbient = { open, close, ownSkip };
3278
+ combi.parse = (input, pos, ctx) => {
3279
+ const amb = ctx.scanSkip;
3280
+ if (!amb || amb.length === 0) return eagerParse(input, pos, ctx);
3281
+ let interior = cache.get(amb);
3282
+ if (!interior) {
3283
+ interior = buildBalancedInterior(open, close, [...amb, ...ownSkip]);
3284
+ cache.set(amb, interior);
3285
+ }
3286
+ return interior.parse(input, pos, ctx);
3287
+ };
3288
+ return combi;
3289
+ }
3290
+ function buildBalancedInterior(open, close, skips) {
3291
+ const self = ref();
3292
+ const stop = /* @__PURE__ */ new Set([open, close]);
3293
+ let bounded = true;
3294
+ for (const sk of skips) {
3295
+ const cs = firstSetClassChars(sk);
3296
+ if (cs === null) {
3297
+ bounded = false;
3298
+ break;
3299
+ }
3300
+ for (const ch2 of cs) stop.add(ch2);
3301
+ }
3302
+ const cls = [...stop].map(escapeClassChar).join("");
3303
+ const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
3304
+ const inner = many(choice(self, ...skips, content));
3305
+ const combi = transform(
3306
+ sequence(literal(open), inner, expect(literal(close))),
3307
+ // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
3308
+ // ParseError (recovered close). `c` is the close string or a ParseError.
3309
+ ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
3310
+ );
3311
+ if (combi._def.tag === "transform") {
3312
+ combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
3313
+ combi._def.recognitionOnly = true;
3314
+ }
3315
+ self.define(combi);
3316
+ return combi;
3317
+ }
3318
+ function firstSetClassChars(c) {
3319
+ const fs = c._meta.firstSet;
3320
+ if (fs.kind === "empty") return [];
3321
+ if (fs.kind !== "ranges") return null;
3322
+ const out = [];
3323
+ for (const { lo, hi } of fs.ranges) {
3324
+ if (hi - lo > 8) return null;
3325
+ for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
3326
+ }
3327
+ return out;
3328
+ }
3329
+ function escapeClassChar(ch2) {
3330
+ return ch2.replace(/[\\\]^-]/g, "\\$&");
3331
+ }
3332
+
2888
3333
  // src/compiler/grammar-coverage-ids.ts
2889
3334
  function children(def, winners) {
2890
3335
  switch (def.tag) {
@@ -2997,16 +3442,16 @@ function literalCodePoints(frag) {
2997
3442
  const out = [];
2998
3443
  let i = 0;
2999
3444
  while (i < frag.length) {
3000
- const ch = frag[i];
3001
- if (ch === "\\") {
3445
+ const ch2 = frag[i];
3446
+ if (ch2 === "\\") {
3002
3447
  const e = frag[i + 1];
3003
3448
  if (e === void 0) return null;
3004
3449
  out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e] : e.codePointAt(0));
3005
3450
  i += 2;
3006
3451
  continue;
3007
3452
  }
3008
- if (META.has(ch)) return null;
3009
- out.push(ch.codePointAt(0));
3453
+ if (META.has(ch2)) return null;
3454
+ out.push(ch2.codePointAt(0));
3010
3455
  i += 1;
3011
3456
  }
3012
3457
  return out.length ? out : null;
@@ -3085,8 +3530,8 @@ function parseSeqParts(source) {
3085
3530
  };
3086
3531
  let i = 0;
3087
3532
  while (i < source.length) {
3088
- const ch = source[i];
3089
- if (ch === "[") {
3533
+ const ch2 = source[i];
3534
+ if (ch2 === "[") {
3090
3535
  let k = i + 1;
3091
3536
  const neg = source[k] === "^";
3092
3537
  if (neg) k++;
@@ -3108,7 +3553,7 @@ function parseSeqParts(source) {
3108
3553
  i += runFrom(ranges, neg, i);
3109
3554
  continue;
3110
3555
  }
3111
- if (ch === "\\") {
3556
+ if (ch2 === "\\") {
3112
3557
  const e = source[i + 1];
3113
3558
  if (e === "d" || e === "w" || e === "s") {
3114
3559
  flush();
@@ -3137,7 +3582,7 @@ function parseSeqParts(source) {
3137
3582
  } else lit.push(cp2);
3138
3583
  continue;
3139
3584
  }
3140
- if (ch === "(") {
3585
+ if (ch2 === "(") {
3141
3586
  if (source[i + 1] !== "?" || source[i + 2] !== ":") return null;
3142
3587
  let k = i + 3;
3143
3588
  let depth = 1;
@@ -3186,9 +3631,9 @@ function parseSeqParts(source) {
3186
3631
  }
3187
3632
  continue;
3188
3633
  }
3189
- if (META.has(ch)) return null;
3190
- const cp = ch.codePointAt(0);
3191
- i += ch.length;
3634
+ if (META.has(ch2)) return null;
3635
+ const cp = ch2.codePointAt(0);
3636
+ i += ch2.length;
3192
3637
  if (source[i] === "?") {
3193
3638
  flush();
3194
3639
  parts.push({ part: "lit", cps: [cp], optional: true });
@@ -3365,12 +3810,12 @@ function unwrapOuterGroup(source) {
3365
3810
  let i = 3;
3366
3811
  let ok = true;
3367
3812
  while (i < source.length && depth > 0) {
3368
- const ch = source[i];
3369
- if (ch === "\\") {
3813
+ const ch2 = source[i];
3814
+ if (ch2 === "\\") {
3370
3815
  i += 2;
3371
3816
  continue;
3372
3817
  }
3373
- if (ch === "[") {
3818
+ if (ch2 === "[") {
3374
3819
  let j = i + 1;
3375
3820
  if (source[j] === "^") j++;
3376
3821
  while (j < source.length && source[j] !== "]") {
@@ -3384,8 +3829,8 @@ function unwrapOuterGroup(source) {
3384
3829
  i = j + 1;
3385
3830
  continue;
3386
3831
  }
3387
- if (ch === "(") depth++;
3388
- else if (ch === ")") depth--;
3832
+ if (ch2 === "(") depth++;
3833
+ else if (ch2 === ")") depth--;
3389
3834
  i++;
3390
3835
  }
3391
3836
  if (!ok || depth !== 0 || i !== source.length) break;
@@ -3400,12 +3845,12 @@ function splitTopLevelAlternation(source) {
3400
3845
  let last = 0;
3401
3846
  let i = 0;
3402
3847
  while (i < body.length) {
3403
- const ch = body[i];
3404
- if (ch === "\\") {
3848
+ const ch2 = body[i];
3849
+ if (ch2 === "\\") {
3405
3850
  i += 2;
3406
3851
  continue;
3407
3852
  }
3408
- if (ch === "[") {
3853
+ if (ch2 === "[") {
3409
3854
  let j = i + 1;
3410
3855
  if (body[j] === "^") j++;
3411
3856
  while (j < body.length && body[j] !== "]") {
@@ -3416,18 +3861,18 @@ function splitTopLevelAlternation(source) {
3416
3861
  i = j + 1;
3417
3862
  continue;
3418
3863
  }
3419
- if (ch === "(") {
3864
+ if (ch2 === "(") {
3420
3865
  depth++;
3421
3866
  i++;
3422
3867
  continue;
3423
3868
  }
3424
- if (ch === ")") {
3869
+ if (ch2 === ")") {
3425
3870
  depth--;
3426
3871
  if (depth < 0) return null;
3427
3872
  i++;
3428
3873
  continue;
3429
3874
  }
3430
- if (ch === "|" && depth === 0) {
3875
+ if (ch2 === "|" && depth === 0) {
3431
3876
  parts.push(body.slice(last, i));
3432
3877
  last = i + 1;
3433
3878
  i++;
@@ -3546,12 +3991,12 @@ function splitDelimArms(body) {
3546
3991
  let last = 0;
3547
3992
  let i = 0;
3548
3993
  while (i < body.length) {
3549
- const ch = body[i];
3550
- if (ch === "\\") {
3994
+ const ch2 = body[i];
3995
+ if (ch2 === "\\") {
3551
3996
  i += 2;
3552
3997
  continue;
3553
3998
  }
3554
- if (ch === "[") {
3999
+ if (ch2 === "[") {
3555
4000
  let j = i + 1;
3556
4001
  if (body[j] === "^") j++;
3557
4002
  while (j < body.length && body[j] !== "]") {
@@ -3562,18 +4007,18 @@ function splitDelimArms(body) {
3562
4007
  i = j + 1;
3563
4008
  continue;
3564
4009
  }
3565
- if (ch === "(") {
4010
+ if (ch2 === "(") {
3566
4011
  depth++;
3567
4012
  i++;
3568
4013
  continue;
3569
4014
  }
3570
- if (ch === ")") {
4015
+ if (ch2 === ")") {
3571
4016
  depth--;
3572
4017
  if (depth < 0) return null;
3573
4018
  i++;
3574
4019
  continue;
3575
4020
  }
3576
- if (ch === "|" && depth === 0) {
4021
+ if (ch2 === "|" && depth === 0) {
3577
4022
  arms.push(body.slice(last, i));
3578
4023
  last = i + 1;
3579
4024
  }
@@ -3625,12 +4070,12 @@ function parseDelimited(source) {
3625
4070
  let k = i + 3;
3626
4071
  let depth = 1;
3627
4072
  while (k < source.length && depth > 0) {
3628
- const ch = source[k];
3629
- if (ch === "\\") {
4073
+ const ch2 = source[k];
4074
+ if (ch2 === "\\") {
3630
4075
  k += 2;
3631
4076
  continue;
3632
4077
  }
3633
- if (ch === "[") {
4078
+ if (ch2 === "[") {
3634
4079
  let j = k + 1;
3635
4080
  if (source[j] === "^") j++;
3636
4081
  while (j < source.length && source[j] !== "]") {
@@ -3641,8 +4086,8 @@ function parseDelimited(source) {
3641
4086
  k = j + 1;
3642
4087
  continue;
3643
4088
  }
3644
- if (ch === "(") depth++;
3645
- else if (ch === ")") depth--;
4089
+ if (ch2 === "(") depth++;
4090
+ else if (ch2 === ")") depth--;
3646
4091
  k++;
3647
4092
  }
3648
4093
  if (depth !== 0) return null;
@@ -4314,58 +4759,88 @@ function tryInlineDestructureTransform(src, valueVars) {
4314
4759
 
4315
4760
  // src/compiler/codegen.ts
4316
4761
  function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
4317
- if (seen.has(p)) return { concrete: empty(), refs: [] };
4318
- seen.add(p);
4319
4762
  const d = p._def;
4763
+ if (d.tag === "lazy") {
4764
+ const name = p._ruleName;
4765
+ if (name !== void 0) return { alts: [[{ set: empty(), nullable: false, ref: name }]] };
4766
+ }
4767
+ if (seen.has(p)) return { alts: [[]] };
4768
+ seen.add(p);
4320
4769
  const rec = (c) => leadingFirstSetRecipe(c, seen);
4321
- const merge2 = (a, b) => ({ concrete: union(a.concrete, b.concrete), refs: [...a.refs, ...b.refs] });
4322
- switch (d.tag) {
4323
- case "lazy": {
4324
- const name = p._ruleName;
4325
- if (name !== void 0) return { concrete: empty(), refs: [name] };
4326
- try {
4327
- return rec(d.thunk());
4328
- } catch {
4329
- return { concrete: any(), refs: [] };
4770
+ const seg = (set, nullable2) => ({ set, nullable: nullable2 });
4771
+ const forceNullable = (r) => ({ alts: r.alts.map((alt) => alt.map((s) => ({ ...s, nullable: true }))) });
4772
+ const collapse = (r, nullable2) => {
4773
+ let set = empty();
4774
+ let hasRef = false;
4775
+ for (const alt of r.alts) {
4776
+ for (const s of alt) {
4777
+ if (s.ref !== void 0) hasRef = true;
4778
+ set = union(set, s.set);
4779
+ if (!s.nullable) break;
4330
4780
  }
4331
4781
  }
4332
- case "literal":
4333
- case "regex":
4334
- case "keywords":
4335
- return { concrete: p._meta.firstSet, refs: [] };
4336
- case "choice": {
4337
- let out = { concrete: empty(), refs: [] };
4338
- for (const arm of d.parsers) out = merge2(out, rec(arm));
4339
- return out;
4340
- }
4341
- case "sequence": {
4342
- let out = { concrete: empty(), refs: [] };
4343
- for (const term of d.parsers) {
4344
- out = merge2(out, rec(term));
4345
- if (!matchesEmpty(term)) return out;
4782
+ return seg(hasRef ? any() : set, nullable2);
4783
+ };
4784
+ const compute = () => {
4785
+ switch (d.tag) {
4786
+ case "lazy": {
4787
+ try {
4788
+ return rec(d.thunk());
4789
+ } catch {
4790
+ return { alts: [[seg(any(), false)]] };
4791
+ }
4346
4792
  }
4347
- return out;
4793
+ case "literal":
4794
+ case "regex":
4795
+ case "keywords":
4796
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4797
+ case "choice": {
4798
+ const alts = [];
4799
+ for (const arm of d.parsers) alts.push(...rec(arm).alts);
4800
+ return { alts };
4801
+ }
4802
+ case "sequence": {
4803
+ const chain = [];
4804
+ for (const term of d.parsers) {
4805
+ if (!isZeroWidthAssertion(term)) {
4806
+ const tr = rec(term);
4807
+ if (tr.alts.length === 1) chain.push(...tr.alts[0]);
4808
+ else chain.push(collapse(tr, matchesEmpty(term)));
4809
+ }
4810
+ if (!matchesEmpty(term)) return { alts: [chain] };
4811
+ }
4812
+ return { alts: [chain] };
4813
+ }
4814
+ // `many`/`optional` are skippable as a whole → their leading chars start the
4815
+ // sequence but the chain must continue past them (force nullable). `oneOrMore`
4816
+ // requires at least one repetition, so it is NOT made nullable here.
4817
+ case "many":
4818
+ case "optional":
4819
+ return forceNullable(rec(d.parser));
4820
+ case "oneOrMore":
4821
+ case "transform":
4822
+ case "label":
4823
+ case "field":
4824
+ case "trivia":
4825
+ case "token":
4826
+ case "leaf":
4827
+ case "node":
4828
+ case "grammar":
4829
+ case "expect":
4830
+ return rec(d.parser);
4831
+ case "sepBy":
4832
+ return rec(d.parser);
4833
+ case "skip":
4834
+ return rec(d.main);
4835
+ // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4836
+ default:
4837
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4348
4838
  }
4349
- case "oneOrMore":
4350
- case "many":
4351
- case "optional":
4352
- case "transform":
4353
- case "label":
4354
- case "field":
4355
- case "trivia":
4356
- case "token":
4357
- case "leaf":
4358
- case "node":
4359
- case "grammar":
4360
- case "expect":
4361
- return rec(d.parser);
4362
- case "sepBy":
4363
- return rec(d.parser);
4364
- case "skip":
4365
- return rec(d.main);
4366
- // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4367
- default:
4368
- return { concrete: p._meta.firstSet, refs: [] };
4839
+ };
4840
+ try {
4841
+ return compute();
4842
+ } finally {
4843
+ seen.delete(p);
4369
4844
  }
4370
4845
  }
4371
4846
  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 }";
@@ -4561,16 +5036,19 @@ function ensureTriviaFn(ctx) {
4561
5036
  const savedIndent = ctx.indent;
4562
5037
  const savedFailLabel = ctx.failLabel;
4563
5038
  const savedTrivia = ctx.activeTrivia;
5039
+ const savedScanSkip = ctx.activeScanSkip;
4564
5040
  const savedCapAsTrivia = ctx.capAsTrivia;
4565
5041
  ctx.indent = 2;
4566
5042
  ctx.failLabel = "_triv";
4567
5043
  ctx.capAsTrivia = true;
4568
5044
  ctx.activeTrivia = void 0;
5045
+ ctx.activeScanSkip = void 0;
4569
5046
  const r = emit(trivia2, ctx, "_pos");
4570
5047
  ctx.indent = savedIndent;
4571
5048
  ctx.failLabel = savedFailLabel;
4572
5049
  ctx.capAsTrivia = savedCapAsTrivia;
4573
5050
  ctx.activeTrivia = savedTrivia;
5051
+ ctx.activeScanSkip = savedScanSkip;
4574
5052
  ctx.namedFnDecls.push([
4575
5053
  `function ${fnName}(input, _pos, _ctx, _cap) {`,
4576
5054
  ` let _e = _pos`,
@@ -4848,7 +5326,7 @@ function emitKeywordsFast(def, ctx, pos) {
4848
5326
  const bodyInd = ind(ctx) + " ";
4849
5327
  const tries = [];
4850
5328
  for (const w of def.words) {
4851
- const cps = Array.from(w, (ch) => ch.codePointAt(0));
5329
+ const cps = Array.from(w, (ch2) => ch2.codePointAt(0));
4852
5330
  let shape = def.caseInsensitive ? { kind: "litFold", open: cps } : { kind: "seq", parts: [{ part: "lit", cps, optional: false }] };
4853
5331
  if (boundary) {
4854
5332
  shape = { kind: "lookahead", inner: shape, ranges: boundary.ranges, classNegated: boundary.negated, negative: true };
@@ -5041,7 +5519,7 @@ function canMatchEmptyAtStart(p) {
5041
5519
  const d = p._def;
5042
5520
  switch (d.tag) {
5043
5521
  case "regex":
5044
- return /(?:[*?]|\{0,|\{\d*,)/.test(d.source);
5522
+ return regexMatchesEmpty(d.source, d.flags);
5045
5523
  case "optional":
5046
5524
  return true;
5047
5525
  case "many":
@@ -5093,10 +5571,10 @@ function emitDisjointArm(p, ctx, pos, valV, endV, coverageId) {
5093
5571
  }
5094
5572
  function emitDisjointArmGated(def, i, ctx, pos, valV, endV, coverageId) {
5095
5573
  const p = def.parsers[i];
5096
- const gate = def.gates[i];
5097
- if (!gate) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
5574
+ const gate2 = def.gates[i];
5575
+ if (!gate2) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
5098
5576
  const gateIdx = ctx.mapFns.length;
5099
- ctx.mapFns.push(gate);
5577
+ ctx.mapFns.push(gate2);
5100
5578
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5101
5579
  const gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5102
5580
  const stmts = [`${ind(ctx)}if (${gateCond}) {`];
@@ -5254,7 +5732,7 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5254
5732
  if (preStmts) stmts.push(...preStmts);
5255
5733
  for (let i = 0; i < def.parsers.length; i++) {
5256
5734
  const p = def.parsers[i];
5257
- const gate = def.gates[i];
5735
+ const gate2 = def.gates[i];
5258
5736
  const autoNot = def.autoNot[i];
5259
5737
  const atStart = failsAtStart(p);
5260
5738
  const staticFx = armStaticExpected(ctx, p);
@@ -5266,9 +5744,9 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5266
5744
  })();
5267
5745
  const fsGuard = armGuardPrefix === void 0 ? baseFsGuard : baseFsGuard === null ? armGuardPrefix : `${armGuardPrefix} && (${baseFsGuard})`;
5268
5746
  let gateCond = null;
5269
- if (gate) {
5747
+ if (gate2) {
5270
5748
  const gateIdx = ctx.mapFns.length;
5271
- ctx.mapFns.push(gate);
5749
+ ctx.mapFns.push(gate2);
5272
5750
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5273
5751
  gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5274
5752
  }
@@ -5685,10 +6163,15 @@ function emitScanTo(def, ctx, pos) {
5685
6163
  const probeCtx = { ...ctx, capturing: false, noHoist: true };
5686
6164
  const { stmts: sentStmts, okVar: sentOk } = emitFallible(def.sentinel, probeCtx, curV);
5687
6165
  stmts.push(...sentStmts, `${ind(ctx)}if (${sentOk}) { ${foundV} = true; break }`);
5688
- if (def.skip.length > 0) {
6166
+ const effectiveSkip = def.raw ? def.skip : [
6167
+ ...ctx.activeTrivia ? [ctx.activeTrivia] : [],
6168
+ ...ctx.activeScanSkip ?? [],
6169
+ ...def.skip
6170
+ ];
6171
+ if (effectiveSkip.length > 0) {
5689
6172
  const advV = v(ctx, "_stadv");
5690
6173
  stmts.push(`${ind(ctx)}let ${advV} = false`);
5691
- for (const skipper of def.skip) {
6174
+ for (const skipper of effectiveSkip) {
5692
6175
  const { stmts: skStmts, okVar: skOk, endVar: skEnd } = emitFallible(skipper, probeCtx, curV);
5693
6176
  stmts.push(
5694
6177
  `${ind(ctx)}if (!${advV}) {`,
@@ -6158,6 +6641,24 @@ function emit(p, ctx, pos) {
6158
6641
  return instrument(dispatch());
6159
6642
  }
6160
6643
  function emitDispatch(p, ctx, pos) {
6644
+ const bal = p._balancedAmbient;
6645
+ if (bal && ctx.activeScanSkip && ctx.activeScanSkip.length > 0 && !ctx.balancedRebuildStack?.has(p)) {
6646
+ const interior = buildBalancedInterior(bal.open, bal.close, [...ctx.activeScanSkip, ...bal.ownSkip]);
6647
+ markUnusedValues(interior);
6648
+ if (ctx.lazyUsage) {
6649
+ const u = analyzeLazyUsage(interior);
6650
+ for (const [k, n] of u.counts) ctx.lazyUsage.counts.set(k, (ctx.lazyUsage.counts.get(k) ?? 0) + n);
6651
+ for (const k of u.recursive) ctx.lazyUsage.recursive.add(k);
6652
+ for (const [k, s] of u.sizes) ctx.lazyUsage.sizes.set(k, s);
6653
+ }
6654
+ const stack = ctx.balancedRebuildStack ?? (ctx.balancedRebuildStack = /* @__PURE__ */ new Set());
6655
+ stack.add(p);
6656
+ try {
6657
+ return emit(interior, ctx, pos);
6658
+ } finally {
6659
+ stack.delete(p);
6660
+ }
6661
+ }
6161
6662
  const def = p._def;
6162
6663
  switch (def.tag) {
6163
6664
  case "literal":
@@ -6579,9 +7080,36 @@ function ruleDependencies(ruleMap) {
6579
7080
  }
6580
7081
  return deps;
6581
7082
  }
7083
+ function resolveGatingLevel(opt) {
7084
+ const explicit = typeof opt === "string" ? opt : opt?.level;
7085
+ if (explicit !== void 0) return explicit;
7086
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_GATING : void 0;
7087
+ if (env === "off" || env === "warn" || env === "error") return env;
7088
+ return "warn";
7089
+ }
7090
+ function runGatingDiagnostic(combinator, opt) {
7091
+ const level = resolveGatingLevel(opt);
7092
+ if (level === "off") return void 0;
7093
+ const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7094
+ let report;
7095
+ try {
7096
+ report = analyzeGating(combinator, accept ? { accept } : void 0);
7097
+ } catch {
7098
+ return void 0;
7099
+ }
7100
+ const lines = formatGatingWarnings(report);
7101
+ if (lines.length > 0) {
7102
+ if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
7103
+ ${lines.join("\n")}`);
7104
+ for (const l of lines) console.warn(l);
7105
+ }
7106
+ return report;
7107
+ }
6582
7108
  function compile(combinator, mapFnSources, opts) {
7109
+ const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
6583
7110
  markUnusedValues(combinator);
6584
7111
  const grammarTrivia = combinator._meta.grammarTrivia;
7112
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
6585
7113
  const ctx = {
6586
7114
  vars: 0,
6587
7115
  indent: 1,
@@ -6603,7 +7131,8 @@ function compile(combinator, mapFnSources, opts) {
6603
7131
  recovery: opts?.recovery ?? false,
6604
7132
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
6605
7133
  lazyUsage: analyzeLazyUsage(combinator),
6606
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7134
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7135
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6607
7136
  };
6608
7137
  const r = emit(combinator, ctx, "_pos");
6609
7138
  const coverageRootRuleId = ctx.coverage?.plan.rules.get(combinator);
@@ -6667,6 +7196,7 @@ function compile(combinator, mapFnSources, opts) {
6667
7196
  source,
6668
7197
  inlineExpression,
6669
7198
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
7199
+ ...gatingReport === void 0 ? {} : { gating: gatingReport },
6670
7200
  parse(input, pos = 0) {
6671
7201
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
6672
7202
  },
@@ -6702,6 +7232,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6702
7232
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
6703
7233
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
6704
7234
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7235
+ const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6705
7236
  const ruleMap = ruleMapArg.filter(([, val]) => {
6706
7237
  const d = val._def;
6707
7238
  if (d.tag !== "lazy") return true;
@@ -6739,7 +7270,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
6739
7270
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6740
7271
  ns,
6741
7272
  deferFirstSetRefs: true,
6742
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7273
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7274
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6743
7275
  };
6744
7276
  const seen = /* @__PURE__ */ new Map();
6745
7277
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6803,9 +7335,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
6803
7335
  ctx.failLabel = "_pfail";
6804
7336
  ctx.recordFail = true;
6805
7337
  const savedTrivia = ctx.activeTrivia;
6806
- if (rule._meta.isTrivia) ctx.activeTrivia = void 0;
7338
+ const savedScanSkip = ctx.activeScanSkip;
7339
+ if (rule._meta.isTrivia) {
7340
+ ctx.activeTrivia = void 0;
7341
+ ctx.activeScanSkip = void 0;
7342
+ }
6807
7343
  const body = emit(resolved, ctx, "_pos");
6808
7344
  ctx.activeTrivia = savedTrivia;
7345
+ ctx.activeScanSkip = savedScanSkip;
6809
7346
  ctx.indent = savedIndent;
6810
7347
  ctx.failLabel = savedFail;
6811
7348
  ctx.recordFail = savedRec;
@@ -6852,6 +7389,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6852
7389
  }
6853
7390
  const firstSets = /* @__PURE__ */ new Map();
6854
7391
  const firstSetRecipes = /* @__PURE__ */ new Map();
7392
+ const nullable2 = /* @__PURE__ */ new Map();
6855
7393
  for (const [key, rule] of ruleMap) {
6856
7394
  let resolved = rule;
6857
7395
  const d = rule._def;
@@ -6863,15 +7401,18 @@ function compileLinkable(ruleMapArg, ns, opts) {
6863
7401
  }
6864
7402
  }
6865
7403
  firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
6866
- firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { concrete: any(), refs: [] } : leadingFirstSetRecipe(resolved));
7404
+ nullable2.set(key, matchesEmpty(resolved));
7405
+ firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { alts: [[{ set: any(), nullable: false }]] } : leadingFirstSetRecipe(resolved));
6867
7406
  }
6868
7407
  return {
7408
+ v: PARSEMAN_VERSION,
6869
7409
  ns,
6870
7410
  keys: perEntry.map((e) => e.key),
6871
7411
  prelude,
6872
7412
  ruleFns,
6873
7413
  wrappers,
6874
7414
  firstSets,
7415
+ nullable: nullable2,
6875
7416
  firstSetRecipes,
6876
7417
  deps: ruleDependencies(ruleMap),
6877
7418
  needsEmptyTl: !!ctx.needsEmptyTl,
@@ -6950,101 +7491,6 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
6950
7491
  ].join("\n");
6951
7492
  }
6952
7493
 
6953
- // src/combinators/scanTo.ts
6954
- function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
6955
- const meta = {
6956
- firstSet: any(),
6957
- canMatchNewline: true,
6958
- isTrivia: false
6959
- };
6960
- return {
6961
- _tag: "scanTo",
6962
- _meta: meta,
6963
- _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
6964
- parse(input, pos, ctx) {
6965
- let cur = pos;
6966
- const probeCtx = {
6967
- trackLines: ctx.trackLines,
6968
- state: ctx.state,
6969
- ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
6970
- };
6971
- const emit2 = (end) => {
6972
- if (end > pos && cstCaptureActive(ctx)) {
6973
- const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
6974
- pushCstLeaf(ctx, leaf2);
6975
- }
6976
- };
6977
- while (cur < input.length) {
6978
- const s = sentinel.parse(input, cur, probeCtx);
6979
- if (s.ok) {
6980
- emit2(cur);
6981
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6982
- }
6983
- let advanced = false;
6984
- for (const skipper of skip2) {
6985
- const r = skipper.parse(input, cur, probeCtx);
6986
- if (r.ok && r.span.end > cur) {
6987
- cur = r.span.end;
6988
- advanced = true;
6989
- break;
6990
- }
6991
- }
6992
- if (!advanced) cur++;
6993
- }
6994
- if (orEOF) {
6995
- emit2(cur);
6996
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6997
- }
6998
- const sentDef = sentinel._def;
6999
- const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
7000
- return { ok: false, expected, span: { start: pos, end: cur } };
7001
- }
7002
- };
7003
- }
7004
- function balanced(open, close, options = {}) {
7005
- const self = ref();
7006
- const skips = options.skip ?? [];
7007
- const stop = /* @__PURE__ */ new Set([open, close]);
7008
- let bounded = true;
7009
- for (const sk of skips) {
7010
- const cs = firstSetClassChars(sk);
7011
- if (cs === null) {
7012
- bounded = false;
7013
- break;
7014
- }
7015
- for (const ch of cs) stop.add(ch);
7016
- }
7017
- const cls = [...stop].map(escapeClassChar).join("");
7018
- const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
7019
- const inner = many(choice(self, ...skips, content));
7020
- const combi = transform(
7021
- sequence(literal(open), inner, expect(literal(close))),
7022
- // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
7023
- // ParseError (recovered close). `c` is the close string or a ParseError.
7024
- ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
7025
- );
7026
- if (combi._def.tag === "transform") {
7027
- combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
7028
- combi._def.recognitionOnly = true;
7029
- }
7030
- self.define(combi);
7031
- return combi;
7032
- }
7033
- function firstSetClassChars(c) {
7034
- const fs = c._meta.firstSet;
7035
- if (fs.kind === "empty") return [];
7036
- if (fs.kind !== "ranges") return null;
7037
- const out = [];
7038
- for (const { lo, hi } of fs.ranges) {
7039
- if (hi - lo > 8) return null;
7040
- for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
7041
- }
7042
- return out;
7043
- }
7044
- function escapeClassChar(ch) {
7045
- return ch.replace(/[\\\]^-]/g, "\\$&");
7046
- }
7047
-
7048
7494
  // src/combinators/withCtx.ts
7049
7495
  function withCtx(extra, combinator) {
7050
7496
  const meta = {
@@ -7101,7 +7547,7 @@ function childrenOf2(def) {
7101
7547
  return [];
7102
7548
  }
7103
7549
  }
7104
- var ruleNameOf = (c) => c._ruleName;
7550
+ var ruleNameOf2 = (c) => c._ruleName;
7105
7551
  function lazyTarget(c) {
7106
7552
  if (c._def.tag !== "lazy") return null;
7107
7553
  try {
@@ -7168,15 +7614,15 @@ function evalRuleMapIR(ir) {
7168
7614
  if (Array.isArray(it)) {
7169
7615
  const [src, comb] = it;
7170
7616
  gateSrcs.push(src);
7171
- let gate;
7617
+ let gate2;
7172
7618
  try {
7173
- gate = (0, eval)(`(${src})`);
7619
+ gate2 = (0, eval)(`(${src})`);
7174
7620
  } catch {
7175
- gate = () => {
7621
+ gate2 = () => {
7176
7622
  throw new Error("IR gate fn not materialized");
7177
7623
  };
7178
7624
  }
7179
- return { gate, combinator: comb };
7625
+ return { gate: gate2, combinator: comb };
7180
7626
  }
7181
7627
  gateSrcs.push(null);
7182
7628
  return it;
@@ -7283,7 +7729,7 @@ var Serializer = class {
7283
7729
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7284
7730
  * `g[name]` ref — only the top-level entry is unwrapped. */
7285
7731
  body(c) {
7286
- return c._def.tag === "lazy" && ruleNameOf(c) !== void 0 ? lazyTarget(c) ?? c : c;
7732
+ return c._def.tag === "lazy" && ruleNameOf2(c) !== void 0 ? lazyTarget(c) ?? c : c;
7287
7733
  }
7288
7734
  run() {
7289
7735
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
@@ -7308,7 +7754,7 @@ ${body}
7308
7754
  analyze(c, active) {
7309
7755
  const def = c._def;
7310
7756
  if (def.tag === "lazy") {
7311
- if (ruleNameOf(c) !== void 0) return;
7757
+ if (ruleNameOf2(c) !== void 0) return;
7312
7758
  const target = lazyTarget(c);
7313
7759
  if (!target) throw new Unserializable("unresolved unnamed ref");
7314
7760
  if (active.has(target)) {
@@ -7360,7 +7806,7 @@ ${body}
7360
7806
  const kid = (x) => this.ref(x, selfOf);
7361
7807
  switch (def.tag) {
7362
7808
  case "lazy": {
7363
- if (ruleNameOf(c) !== void 0) return `g[${JSON.stringify(ruleNameOf(c))}]`;
7809
+ if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
7364
7810
  const target = lazyTarget(c);
7365
7811
  if (selfOf && target === selfOf.comb) return selfOf.var;
7366
7812
  return this.ref(target, selfOf);
@@ -7414,7 +7860,7 @@ ${body}
7414
7860
  case "skip":
7415
7861
  return `skip(${kid(def.main)}, ${kid(def.skipped)})`;
7416
7862
  case "scanTo":
7417
- return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}], orEOF: ${def.orEOF} })`;
7863
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}]${def.raw ? ", raw: true" : ""}, orEOF: ${def.orEOF} })`;
7418
7864
  case "transform": {
7419
7865
  if (def.fnSrc === void 0) throw new Unserializable("transform without fnSrc");
7420
7866
  return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ", true" : ""})`;
@@ -7484,6 +7930,18 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
7484
7930
  }
7485
7931
  cstBuildHost._parsemanCstOutput = true;
7486
7932
  function fusedBody(pieces) {
7933
+ for (const p of pieces) {
7934
+ if (p.v === void 0) {
7935
+ throw new Error(
7936
+ `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.`
7937
+ );
7938
+ }
7939
+ if (p.v !== PARSEMAN_VERSION) {
7940
+ throw new Error(
7941
+ `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.`
7942
+ );
7943
+ }
7944
+ }
7487
7945
  const winner = /* @__PURE__ */ new Map();
7488
7946
  for (const p of pieces) for (const k of p.keys) winner.set(k, p);
7489
7947
  for (const [k, p] of winner) {
@@ -7509,23 +7967,38 @@ function fusedBody(pieces) {
7509
7967
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
7510
7968
  const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
7511
7969
  const ANY2 = { kind: "any" };
7970
+ const EMPTY3 = { kind: "empty" };
7512
7971
  const recipes = /* @__PURE__ */ new Map();
7513
7972
  const shallow = /* @__PURE__ */ new Map();
7973
+ const nullable2 = /* @__PURE__ */ new Map();
7514
7974
  for (const [k, p] of winner) {
7515
7975
  const r = p.firstSetRecipes?.get(k);
7516
7976
  if (r) recipes.set(k, r);
7517
7977
  const fs = p.firstSets?.get(k);
7518
7978
  if (fs) shallow.set(k, fs);
7519
- }
7979
+ const nu = p.nullable?.get(k);
7980
+ if (nu !== void 0) nullable2.set(k, nu);
7981
+ }
7982
+ const knownRef = (ref2) => recipes.has(ref2) || shallow.has(ref2);
7983
+ const refFS = (ref2) => knownRef(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2;
7984
+ const refNullable = (ref2) => knownRef(ref2) ? nullable2.get(ref2) ?? true : true;
7985
+ const resolveRecipe = (r) => {
7986
+ let out = EMPTY3;
7987
+ for (const chain of r.alts) {
7988
+ for (const seg of chain) {
7989
+ out = union(out, seg.ref !== void 0 ? refFS(seg.ref) : seg.set);
7990
+ const skippable = seg.ref !== void 0 ? seg.nullable || refNullable(seg.ref) : seg.nullable;
7991
+ if (!skippable) break;
7992
+ }
7993
+ }
7994
+ return out;
7995
+ };
7520
7996
  const finalFS = /* @__PURE__ */ new Map();
7521
- for (const [k] of winner) finalFS.set(k, recipes.get(k)?.concrete ?? shallow.get(k) ?? ANY2);
7997
+ for (const [k] of winner) finalFS.set(k, recipes.has(k) ? EMPTY3 : shallow.get(k) ?? ANY2);
7522
7998
  for (let iter = 0; iter <= recipes.size; iter++) {
7523
7999
  let changed = false;
7524
8000
  for (const [k, r] of recipes) {
7525
- let fs = r.concrete;
7526
- for (const ref2 of r.refs) {
7527
- fs = union(fs, recipes.has(ref2) || shallow.has(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2);
7528
- }
8001
+ const fs = resolveRecipe(r);
7529
8002
  if (JSON.stringify(finalFS.get(k)) !== JSON.stringify(fs)) {
7530
8003
  finalFS.set(k, fs);
7531
8004
  changed = true;
@@ -7657,8 +8130,8 @@ function composeLeaf(_items) {
7657
8130
  throw new Error("composeLeaf(): requires Parseman macro lowering; runtime composition is forbidden");
7658
8131
  }
7659
8132
 
7660
- // src/combinators/guard.ts
7661
- function guard(predicate) {
8133
+ // src/combinators/gate.ts
8134
+ function gate(predicate) {
7662
8135
  const meta = {
7663
8136
  firstSet: { kind: "any" },
7664
8137
  canMatchNewline: false,
@@ -7676,6 +8149,9 @@ function guard(predicate) {
7676
8149
  };
7677
8150
  }
7678
8151
 
8152
+ // src/combinators/guard.ts
8153
+ var guard = gate;
8154
+
7679
8155
  // src/combinators/completions.ts
7680
8156
  function completionsAt(target, input, offset, options = {}) {
7681
8157
  const probe = { offset, best: null };
@@ -8207,12 +8683,14 @@ function runOnce(entry, input, options, phase, profileState) {
8207
8683
  const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
8208
8684
  const skipGlobalSinks = phase === "recognizer" || phase === "capture";
8209
8685
  const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
8686
+ const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
8210
8687
  const ctx = {
8211
8688
  trackLines: false,
8212
8689
  ...skipGlobalSinks ? { _pmProfile: profile } : { _triviaLog: triviaLog, _errors: errors, ...profile === void 0 ? {} : { _pmProfile: profile } },
8213
8690
  build: options.build,
8214
8691
  state: options.state,
8215
8692
  ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...grammarTrivia._meta.triviaKindLabels ? { triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {} } : {},
8693
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
8216
8694
  ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
8217
8695
  ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
8218
8696
  ...options.instrumentation === void 0 ? {} : options.instrumentation
@@ -8498,7 +8976,7 @@ function coverageEntry(entry, collector, maps, trace) {
8498
8976
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
8499
8977
  }
8500
8978
  case "guard":
8501
- return guard(def.predicate);
8979
+ return gate(def.predicate);
8502
8980
  case "withCtx":
8503
8981
  return withCtx(def.extra, build(def.parser));
8504
8982
  case "expect":
@@ -8553,9 +9031,9 @@ function buildTriviaIndex(root, input = "", opts) {
8553
9031
  if (prev) merge(after, prev.span.end, run2);
8554
9032
  }
8555
9033
  }
8556
- const ch = node2.children;
8557
- if (ch) {
8558
- for (const c of ch) {
9034
+ const ch2 = node2.children;
9035
+ if (ch2) {
9036
+ for (const c of ch2) {
8559
9037
  if (typeof c === "object" && c !== null && c.children !== void 0) {
8560
9038
  visit(c);
8561
9039
  }