parseman 0.30.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 +721 -241
  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 +721 -241
  24. package/dist/index.js.map +4 -4
  25. package/dist/language-service/index.cjs +3 -1
  26. package/dist/language-service/index.cjs.map +2 -2
  27. package/dist/language-service/index.js +3 -1
  28. package/dist/language-service/index.js.map +2 -2
  29. package/dist/plugin/index.cjs +747 -253
  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 +747 -253
  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 +32 -1
  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 }";
@@ -4500,9 +4975,9 @@ function emitLeafCapture(ctx, valExpr, startExpr, endExpr) {
4500
4975
  const i = ind(ctx);
4501
4976
  const lf = v(ctx, "_lf");
4502
4977
  return [
4503
- `${i}if (_ctx._cstLeaves) {`,
4978
+ `${i}if (_ctx._cstLeaves || _ctx._cstRawChildren) {`,
4504
4979
  `${i} const ${lf} = { _tag: 'leaf', value: ${valExpr}, span: { start: ${startExpr}, end: ${endExpr} } }`,
4505
- `${i} _ctx._cstLeaves.push(${lf})`,
4980
+ `${i} if (_ctx._cstLeaves) _ctx._cstLeaves.push(${lf})`,
4506
4981
  `${i} if (_ctx._cstRawChildren) _ctx._cstRawChildren.push(${lf})`,
4507
4982
  `${i}}`
4508
4983
  ];
@@ -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}) {`,
@@ -5906,8 +6389,10 @@ function emitNode(def, ctx, pos) {
5906
6389
  const directCstV = !structural && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
5907
6390
  const directCstGate = directCstV === null ? "false" : `${profileCapture} || _ctx.build?._parsemanCstOutput === true`;
5908
6391
  const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
6392
+ const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
6393
+ const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
5909
6394
  const allocStmt = structural ? `${i}const ${capTLv} = !(${profileRecognizer}) && (${profileCapture} || ${structuralCapturesTrivia ? "true" : hostTriviaGate}), ${capSTv} = !(${profileRecognizer} || ${profileCapture}) && (_ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 6)))${capFv ? `, ${capFv} = !(${profileRecognizer}) && (${profileCapture} || (_ctx.build !== undefined && _hostReads(_ctx.build, 2)))` : ""}
5910
- ${i}const ${chV} = ${profileRecognizer} ? undefined : [], ${rawV} = ${profileRecognizer} ? undefined : [], ${tlV} = ${profileRecognizer} ? undefined : ${innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`}` : capturesTrivia ? `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
6395
+ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [], ${tlV} = ${profileRecognizer} ? undefined : ${innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`}` : capturesTrivia ? `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
5911
6396
  ` : ""}${i}const ${capTLv} = !(${profileRecognizer}), ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}, ${tlV} = ${profileRecognizer} ? undefined : []` : `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
5912
6397
  ` : ""}${i}const ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}`;
5913
6398
  const innerTl = structural || capturesTrivia ? structural && !innerEnablesTriviaCapture ? `${capTLv} ? ${tlV} : undefined` : tlV : "undefined";
@@ -6156,6 +6641,24 @@ function emit(p, ctx, pos) {
6156
6641
  return instrument(dispatch());
6157
6642
  }
6158
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
+ }
6159
6662
  const def = p._def;
6160
6663
  switch (def.tag) {
6161
6664
  case "literal":
@@ -6577,9 +7080,36 @@ function ruleDependencies(ruleMap) {
6577
7080
  }
6578
7081
  return deps;
6579
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
+ }
6580
7108
  function compile(combinator, mapFnSources, opts) {
7109
+ const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
6581
7110
  markUnusedValues(combinator);
6582
7111
  const grammarTrivia = combinator._meta.grammarTrivia;
7112
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
6583
7113
  const ctx = {
6584
7114
  vars: 0,
6585
7115
  indent: 1,
@@ -6601,7 +7131,8 @@ function compile(combinator, mapFnSources, opts) {
6601
7131
  recovery: opts?.recovery ?? false,
6602
7132
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
6603
7133
  lazyUsage: analyzeLazyUsage(combinator),
6604
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7134
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7135
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6605
7136
  };
6606
7137
  const r = emit(combinator, ctx, "_pos");
6607
7138
  const coverageRootRuleId = ctx.coverage?.plan.rules.get(combinator);
@@ -6665,6 +7196,7 @@ function compile(combinator, mapFnSources, opts) {
6665
7196
  source,
6666
7197
  inlineExpression,
6667
7198
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
7199
+ ...gatingReport === void 0 ? {} : { gating: gatingReport },
6668
7200
  parse(input, pos = 0) {
6669
7201
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
6670
7202
  },
@@ -6700,6 +7232,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6700
7232
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
6701
7233
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
6702
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);
6703
7236
  const ruleMap = ruleMapArg.filter(([, val]) => {
6704
7237
  const d = val._def;
6705
7238
  if (d.tag !== "lazy") return true;
@@ -6737,7 +7270,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
6737
7270
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6738
7271
  ns,
6739
7272
  deferFirstSetRefs: true,
6740
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7273
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7274
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6741
7275
  };
6742
7276
  const seen = /* @__PURE__ */ new Map();
6743
7277
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6801,9 +7335,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
6801
7335
  ctx.failLabel = "_pfail";
6802
7336
  ctx.recordFail = true;
6803
7337
  const savedTrivia = ctx.activeTrivia;
6804
- 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
+ }
6805
7343
  const body = emit(resolved, ctx, "_pos");
6806
7344
  ctx.activeTrivia = savedTrivia;
7345
+ ctx.activeScanSkip = savedScanSkip;
6807
7346
  ctx.indent = savedIndent;
6808
7347
  ctx.failLabel = savedFail;
6809
7348
  ctx.recordFail = savedRec;
@@ -6850,6 +7389,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6850
7389
  }
6851
7390
  const firstSets = /* @__PURE__ */ new Map();
6852
7391
  const firstSetRecipes = /* @__PURE__ */ new Map();
7392
+ const nullable2 = /* @__PURE__ */ new Map();
6853
7393
  for (const [key, rule] of ruleMap) {
6854
7394
  let resolved = rule;
6855
7395
  const d = rule._def;
@@ -6861,15 +7401,18 @@ function compileLinkable(ruleMapArg, ns, opts) {
6861
7401
  }
6862
7402
  }
6863
7403
  firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
6864
- 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));
6865
7406
  }
6866
7407
  return {
7408
+ v: PARSEMAN_VERSION,
6867
7409
  ns,
6868
7410
  keys: perEntry.map((e) => e.key),
6869
7411
  prelude,
6870
7412
  ruleFns,
6871
7413
  wrappers,
6872
7414
  firstSets,
7415
+ nullable: nullable2,
6873
7416
  firstSetRecipes,
6874
7417
  deps: ruleDependencies(ruleMap),
6875
7418
  needsEmptyTl: !!ctx.needsEmptyTl,
@@ -6948,101 +7491,6 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
6948
7491
  ].join("\n");
6949
7492
  }
6950
7493
 
6951
- // src/combinators/scanTo.ts
6952
- function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
6953
- const meta = {
6954
- firstSet: any(),
6955
- canMatchNewline: true,
6956
- isTrivia: false
6957
- };
6958
- return {
6959
- _tag: "scanTo",
6960
- _meta: meta,
6961
- _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
6962
- parse(input, pos, ctx) {
6963
- let cur = pos;
6964
- const probeCtx = {
6965
- trackLines: ctx.trackLines,
6966
- state: ctx.state,
6967
- ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
6968
- };
6969
- const emit2 = (end) => {
6970
- if (end > pos && cstCaptureActive(ctx)) {
6971
- const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
6972
- pushCstLeaf(ctx, leaf2);
6973
- }
6974
- };
6975
- while (cur < input.length) {
6976
- const s = sentinel.parse(input, cur, probeCtx);
6977
- if (s.ok) {
6978
- emit2(cur);
6979
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6980
- }
6981
- let advanced = false;
6982
- for (const skipper of skip2) {
6983
- const r = skipper.parse(input, cur, probeCtx);
6984
- if (r.ok && r.span.end > cur) {
6985
- cur = r.span.end;
6986
- advanced = true;
6987
- break;
6988
- }
6989
- }
6990
- if (!advanced) cur++;
6991
- }
6992
- if (orEOF) {
6993
- emit2(cur);
6994
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6995
- }
6996
- const sentDef = sentinel._def;
6997
- const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
6998
- return { ok: false, expected, span: { start: pos, end: cur } };
6999
- }
7000
- };
7001
- }
7002
- function balanced(open, close, options = {}) {
7003
- const self = ref();
7004
- const skips = options.skip ?? [];
7005
- const stop = /* @__PURE__ */ new Set([open, close]);
7006
- let bounded = true;
7007
- for (const sk of skips) {
7008
- const cs = firstSetClassChars(sk);
7009
- if (cs === null) {
7010
- bounded = false;
7011
- break;
7012
- }
7013
- for (const ch of cs) stop.add(ch);
7014
- }
7015
- const cls = [...stop].map(escapeClassChar).join("");
7016
- const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
7017
- const inner = many(choice(self, ...skips, content));
7018
- const combi = transform(
7019
- sequence(literal(open), inner, expect(literal(close))),
7020
- // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
7021
- // ParseError (recovered close). `c` is the close string or a ParseError.
7022
- ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
7023
- );
7024
- if (combi._def.tag === "transform") {
7025
- combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
7026
- combi._def.recognitionOnly = true;
7027
- }
7028
- self.define(combi);
7029
- return combi;
7030
- }
7031
- function firstSetClassChars(c) {
7032
- const fs = c._meta.firstSet;
7033
- if (fs.kind === "empty") return [];
7034
- if (fs.kind !== "ranges") return null;
7035
- const out = [];
7036
- for (const { lo, hi } of fs.ranges) {
7037
- if (hi - lo > 8) return null;
7038
- for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
7039
- }
7040
- return out;
7041
- }
7042
- function escapeClassChar(ch) {
7043
- return ch.replace(/[\\\]^-]/g, "\\$&");
7044
- }
7045
-
7046
7494
  // src/combinators/withCtx.ts
7047
7495
  function withCtx(extra, combinator) {
7048
7496
  const meta = {
@@ -7099,7 +7547,7 @@ function childrenOf2(def) {
7099
7547
  return [];
7100
7548
  }
7101
7549
  }
7102
- var ruleNameOf = (c) => c._ruleName;
7550
+ var ruleNameOf2 = (c) => c._ruleName;
7103
7551
  function lazyTarget(c) {
7104
7552
  if (c._def.tag !== "lazy") return null;
7105
7553
  try {
@@ -7166,15 +7614,15 @@ function evalRuleMapIR(ir) {
7166
7614
  if (Array.isArray(it)) {
7167
7615
  const [src, comb] = it;
7168
7616
  gateSrcs.push(src);
7169
- let gate;
7617
+ let gate2;
7170
7618
  try {
7171
- gate = (0, eval)(`(${src})`);
7619
+ gate2 = (0, eval)(`(${src})`);
7172
7620
  } catch {
7173
- gate = () => {
7621
+ gate2 = () => {
7174
7622
  throw new Error("IR gate fn not materialized");
7175
7623
  };
7176
7624
  }
7177
- return { gate, combinator: comb };
7625
+ return { gate: gate2, combinator: comb };
7178
7626
  }
7179
7627
  gateSrcs.push(null);
7180
7628
  return it;
@@ -7281,7 +7729,7 @@ var Serializer = class {
7281
7729
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7282
7730
  * `g[name]` ref — only the top-level entry is unwrapped. */
7283
7731
  body(c) {
7284
- 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;
7285
7733
  }
7286
7734
  run() {
7287
7735
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
@@ -7306,7 +7754,7 @@ ${body}
7306
7754
  analyze(c, active) {
7307
7755
  const def = c._def;
7308
7756
  if (def.tag === "lazy") {
7309
- if (ruleNameOf(c) !== void 0) return;
7757
+ if (ruleNameOf2(c) !== void 0) return;
7310
7758
  const target = lazyTarget(c);
7311
7759
  if (!target) throw new Unserializable("unresolved unnamed ref");
7312
7760
  if (active.has(target)) {
@@ -7358,7 +7806,7 @@ ${body}
7358
7806
  const kid = (x) => this.ref(x, selfOf);
7359
7807
  switch (def.tag) {
7360
7808
  case "lazy": {
7361
- if (ruleNameOf(c) !== void 0) return `g[${JSON.stringify(ruleNameOf(c))}]`;
7809
+ if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
7362
7810
  const target = lazyTarget(c);
7363
7811
  if (selfOf && target === selfOf.comb) return selfOf.var;
7364
7812
  return this.ref(target, selfOf);
@@ -7412,7 +7860,7 @@ ${body}
7412
7860
  case "skip":
7413
7861
  return `skip(${kid(def.main)}, ${kid(def.skipped)})`;
7414
7862
  case "scanTo":
7415
- 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} })`;
7416
7864
  case "transform": {
7417
7865
  if (def.fnSrc === void 0) throw new Unserializable("transform without fnSrc");
7418
7866
  return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ", true" : ""})`;
@@ -7475,13 +7923,25 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
7475
7923
  return buildCstNode(typeOrOptions, children2 ?? [], _fields, span ?? { start: 0, end: 0 }, rawChildren, triviaLog, state);
7476
7924
  }
7477
7925
  const collapse = normalizeCstCollapse(typeOrOptions?.collapse);
7478
- const host = (type, children3, fields, span2, rawChildren2, triviaLog2, state2) => buildCstNode(type, children3, fields, span2, rawChildren2, triviaLog2, state2);
7926
+ const host = (type, children3, fields, span2, rawChildren2, triviaLog2, state2) => buildCstNode(type, children3 ?? [], fields, span2, rawChildren2, triviaLog2, state2);
7479
7927
  host._parsemanCstOutput = true;
7480
7928
  if (collapse) host._parsemanCstCollapse = collapse;
7481
7929
  return host;
7482
7930
  }
7483
7931
  cstBuildHost._parsemanCstOutput = true;
7484
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
+ }
7485
7945
  const winner = /* @__PURE__ */ new Map();
7486
7946
  for (const p of pieces) for (const k of p.keys) winner.set(k, p);
7487
7947
  for (const [k, p] of winner) {
@@ -7507,23 +7967,38 @@ function fusedBody(pieces) {
7507
7967
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
7508
7968
  const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
7509
7969
  const ANY2 = { kind: "any" };
7970
+ const EMPTY3 = { kind: "empty" };
7510
7971
  const recipes = /* @__PURE__ */ new Map();
7511
7972
  const shallow = /* @__PURE__ */ new Map();
7973
+ const nullable2 = /* @__PURE__ */ new Map();
7512
7974
  for (const [k, p] of winner) {
7513
7975
  const r = p.firstSetRecipes?.get(k);
7514
7976
  if (r) recipes.set(k, r);
7515
7977
  const fs = p.firstSets?.get(k);
7516
7978
  if (fs) shallow.set(k, fs);
7517
- }
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
+ };
7518
7996
  const finalFS = /* @__PURE__ */ new Map();
7519
- 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);
7520
7998
  for (let iter = 0; iter <= recipes.size; iter++) {
7521
7999
  let changed = false;
7522
8000
  for (const [k, r] of recipes) {
7523
- let fs = r.concrete;
7524
- for (const ref2 of r.refs) {
7525
- fs = union(fs, recipes.has(ref2) || shallow.has(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2);
7526
- }
8001
+ const fs = resolveRecipe(r);
7527
8002
  if (JSON.stringify(finalFS.get(k)) !== JSON.stringify(fs)) {
7528
8003
  finalFS.set(k, fs);
7529
8004
  changed = true;
@@ -7655,8 +8130,8 @@ function composeLeaf(_items) {
7655
8130
  throw new Error("composeLeaf(): requires Parseman macro lowering; runtime composition is forbidden");
7656
8131
  }
7657
8132
 
7658
- // src/combinators/guard.ts
7659
- function guard(predicate) {
8133
+ // src/combinators/gate.ts
8134
+ function gate(predicate) {
7660
8135
  const meta = {
7661
8136
  firstSet: { kind: "any" },
7662
8137
  canMatchNewline: false,
@@ -7674,6 +8149,9 @@ function guard(predicate) {
7674
8149
  };
7675
8150
  }
7676
8151
 
8152
+ // src/combinators/guard.ts
8153
+ var guard = gate;
8154
+
7677
8155
  // src/combinators/completions.ts
7678
8156
  function completionsAt(target, input, offset, options = {}) {
7679
8157
  const probe = { offset, best: null };
@@ -8205,12 +8683,14 @@ function runOnce(entry, input, options, phase, profileState) {
8205
8683
  const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
8206
8684
  const skipGlobalSinks = phase === "recognizer" || phase === "capture";
8207
8685
  const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
8686
+ const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
8208
8687
  const ctx = {
8209
8688
  trackLines: false,
8210
8689
  ...skipGlobalSinks ? { _pmProfile: profile } : { _triviaLog: triviaLog, _errors: errors, ...profile === void 0 ? {} : { _pmProfile: profile } },
8211
8690
  build: options.build,
8212
8691
  state: options.state,
8213
8692
  ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...grammarTrivia._meta.triviaKindLabels ? { triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {} } : {},
8693
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
8214
8694
  ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
8215
8695
  ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
8216
8696
  ...options.instrumentation === void 0 ? {} : options.instrumentation
@@ -8496,7 +8976,7 @@ function coverageEntry(entry, collector, maps, trace) {
8496
8976
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
8497
8977
  }
8498
8978
  case "guard":
8499
- return guard(def.predicate);
8979
+ return gate(def.predicate);
8500
8980
  case "withCtx":
8501
8981
  return withCtx(def.extra, build(def.parser));
8502
8982
  case "expect":
@@ -8551,9 +9031,9 @@ function buildTriviaIndex(root, input = "", opts) {
8551
9031
  if (prev) merge(after, prev.span.end, run2);
8552
9032
  }
8553
9033
  }
8554
- const ch = node2.children;
8555
- if (ch) {
8556
- for (const c of ch) {
9034
+ const ch2 = node2.children;
9035
+ if (ch2) {
9036
+ for (const c of ch2) {
8557
9037
  if (typeof c === "object" && c !== null && c.children !== void 0) {
8558
9038
  visit(c);
8559
9039
  }