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
@@ -130,10 +130,13 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
130
130
  return true;
131
131
  }
132
132
  }
133
+ function isZeroWidthAssertion(p) {
134
+ return p._def.tag === "not";
135
+ }
133
136
  function sequenceFirstSet(parsers) {
134
137
  let fs2 = empty();
135
138
  for (const p of parsers) {
136
- fs2 = union(fs2, p._meta.firstSet);
139
+ if (!isZeroWidthAssertion(p)) fs2 = union(fs2, p._meta.firstSet);
137
140
  if (!matchesEmpty(p)) return fs2;
138
141
  }
139
142
  return fs2;
@@ -163,7 +166,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
163
166
  case "sequence": {
164
167
  let out = empty();
165
168
  for (const term of d.parsers) {
166
- out = union(out, fs2(term));
169
+ if (!isZeroWidthAssertion(term)) out = union(out, fs2(term));
167
170
  if (!matchesEmpty(term)) return out;
168
171
  }
169
172
  return out;
@@ -488,9 +491,9 @@ var SPACE_RANGES = [
488
491
  [12288, 12288],
489
492
  [65279, 65279]
490
493
  ];
491
- function shorthandRanges(ch) {
492
- if (ch === "d") return [[48, 57]];
493
- if (ch === "s") return SPACE_RANGES;
494
+ function shorthandRanges(ch2) {
495
+ if (ch2 === "d") return [[48, 57]];
496
+ if (ch2 === "s") return SPACE_RANGES;
494
497
  return [[48, 57], [65, 90], [97, 122], [95, 95]];
495
498
  }
496
499
  function readUnicodeEscape(body, i) {
@@ -503,9 +506,9 @@ function parseClassRanges(body) {
503
506
  const ranges = [];
504
507
  let i = 0;
505
508
  const readAtom = () => {
506
- const ch = body[i];
507
- if (ch === void 0) return null;
508
- if (ch === "\\") {
509
+ const ch2 = body[i];
510
+ if (ch2 === void 0) return null;
511
+ if (ch2 === "\\") {
509
512
  const uni = readUnicodeEscape(body, i);
510
513
  if (uni) {
511
514
  i = uni.next;
@@ -519,8 +522,8 @@ function parseClassRanges(body) {
519
522
  if (e >= "a" && e <= "z" || e >= "A" && e <= "Z") return null;
520
523
  return { cp: e.codePointAt(0) };
521
524
  }
522
- i += ch.length;
523
- return { cp: ch.codePointAt(0) };
525
+ i += ch2.length;
526
+ return { cp: ch2.codePointAt(0) };
524
527
  };
525
528
  while (i < body.length) {
526
529
  const lo = readAtom();
@@ -587,12 +590,35 @@ function shortScanner(source, flags) {
587
590
  return minOne && end === pos ? null : end;
588
591
  };
589
592
  }
593
+ function asciiCaseFold(fs2) {
594
+ if (fs2.kind !== "ranges") return fs2;
595
+ const ranges = fs2.ranges.map((r) => ({ lo: r.lo, hi: r.hi }));
596
+ const add = (lo, hi) => {
597
+ if (lo <= hi) ranges.push({ lo, hi });
598
+ };
599
+ for (const r of fs2.ranges) {
600
+ const uLo = Math.max(r.lo, 65), uHi = Math.min(r.hi, 90);
601
+ add(uLo + 32, uHi + 32);
602
+ const lLo = Math.max(r.lo, 97), lHi = Math.min(r.hi, 122);
603
+ add(lLo - 32, lHi - 32);
604
+ }
605
+ ranges.sort((a, b) => a.lo - b.lo);
606
+ const merged = [];
607
+ for (const r of ranges) {
608
+ const last = merged[merged.length - 1];
609
+ if (last && r.lo <= last.hi + 1) last.hi = Math.max(last.hi, r.hi);
610
+ else merged.push({ lo: r.lo, hi: r.hi });
611
+ }
612
+ return { kind: "ranges", ranges: merged };
613
+ }
590
614
  function regex(pattern, flags = "") {
591
615
  const source = typeof pattern === "string" ? pattern : pattern.source;
592
616
  const resolvedFlags = typeof pattern === "string" ? flags : pattern.flags;
593
617
  const anchored = new RegExp(source, "y" + resolvedFlags.replace(/[gy]/g, ""));
594
618
  const scan = shortScanner(source, resolvedFlags);
595
- const { firstSet: firstSet2, canMatchNewline } = (firstSetAnalyzer ?? permissiveFirstSet)(source);
619
+ const raw = (firstSetAnalyzer ?? permissiveFirstSet)(source);
620
+ const firstSet2 = !resolvedFlags.includes("i") ? raw.firstSet : resolvedFlags.includes("u") || resolvedFlags.includes("v") ? any() : asciiCaseFold(raw.firstSet);
621
+ const canMatchNewline = raw.canMatchNewline;
596
622
  const meta = { firstSet: firstSet2, canMatchNewline, isTrivia: false };
597
623
  return {
598
624
  _tag: "regex",
@@ -647,8 +673,8 @@ function parseRegex(src) {
647
673
  const parseSeq = () => {
648
674
  const parts = [];
649
675
  while (i < src.length) {
650
- const ch = src[i];
651
- if (ch === "|" || ch === ")") break;
676
+ const ch2 = src[i];
677
+ if (ch2 === "|" || ch2 === ")") break;
652
678
  parts.push(parseQuantified());
653
679
  }
654
680
  if (parts.length === 0) return EMPTY2;
@@ -656,18 +682,18 @@ function parseRegex(src) {
656
682
  };
657
683
  const parseQuantified = () => {
658
684
  const atom = parseAtom();
659
- const ch = src[i];
685
+ const ch2 = src[i];
660
686
  let min = null;
661
- if (ch === "*") {
687
+ if (ch2 === "*") {
662
688
  min = 0;
663
689
  i++;
664
- } else if (ch === "+") {
690
+ } else if (ch2 === "+") {
665
691
  min = 1;
666
692
  i++;
667
- } else if (ch === "?") {
693
+ } else if (ch2 === "?") {
668
694
  min = 0;
669
695
  i++;
670
- } else if (ch === "{") {
696
+ } else if (ch2 === "{") {
671
697
  min = parseBraceQuantifier();
672
698
  }
673
699
  if (min === null) return atom;
@@ -683,8 +709,8 @@ function parseRegex(src) {
683
709
  return Number.parseInt(m[1], 10);
684
710
  };
685
711
  const parseAtom = () => {
686
- const ch = src[i];
687
- switch (ch) {
712
+ const ch2 = src[i];
713
+ switch (ch2) {
688
714
  case "(":
689
715
  return parseGroup();
690
716
  case "[":
@@ -705,8 +731,8 @@ function parseRegex(src) {
705
731
  return charNode(fromRange(123, 123), false);
706
732
  // a `{` with nothing to quantify is a literal brace
707
733
  default:
708
- i += ch.length;
709
- return charNode(fromRange(ch.codePointAt(0), ch.codePointAt(0)), ch === "\n");
734
+ i += ch2.length;
735
+ return charNode(fromRange(ch2.codePointAt(0), ch2.codePointAt(0)), ch2 === "\n");
710
736
  }
711
737
  };
712
738
  const parseGroup = () => {
@@ -1139,8 +1165,8 @@ function choice(...args) {
1139
1165
  }
1140
1166
  }
1141
1167
  if (idx >= 0) {
1142
- const gate = gates[idx];
1143
- if (gate && !gate(ctx.state)) {
1168
+ const gate2 = gates[idx];
1169
+ if (gate2 && !gate2(ctx.state)) {
1144
1170
  return { ok: false, expected: deriveExpected(parsers[idx]), span: { start: pos, end: pos } };
1145
1171
  }
1146
1172
  const result = parsers[idx].parse(input, pos, ctx);
@@ -1486,11 +1512,11 @@ function tryFastLabeledScan(input, cur, trivia2) {
1486
1512
  function recordTriviaChunks(ctx, chunks) {
1487
1513
  const kinds = ctx.triviaKindLabels;
1488
1514
  const mask = ctx._triviaCaptureMask;
1489
- for (const ch of chunks) {
1490
- pushTriviaLogEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1515
+ for (const ch2 of chunks) {
1516
+ pushTriviaLogEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1491
1517
  if (ctx.captureTrivia && (ctx._cstBuf !== void 0 || ctx._cstTriviaLog !== void 0)) {
1492
- if (mask === void 0 || kinds === void 0 || (mask & 1 << ch.kindIndex) !== 0) {
1493
- pushCstTriviaEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1518
+ if (mask === void 0 || kinds === void 0 || (mask & 1 << ch2.kindIndex) !== 0) {
1519
+ pushCstTriviaEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1494
1520
  }
1495
1521
  }
1496
1522
  }
@@ -2229,6 +2255,12 @@ function rules(a, b) {
2229
2255
  if (rule && !rule._meta.isTrivia) rule._meta.grammarTrivia = options.trivia;
2230
2256
  }
2231
2257
  }
2258
+ if (options?.scanSkip != null) {
2259
+ for (const key of Object.keys(definitions)) {
2260
+ const rule = cache[key];
2261
+ if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
2262
+ }
2263
+ }
2232
2264
  for (const key of Object.keys(definitions)) {
2233
2265
  markUnusedValues(cache[key]);
2234
2266
  }
@@ -2797,6 +2829,413 @@ function leaf(root, fn) {
2797
2829
  };
2798
2830
  }
2799
2831
 
2832
+ // src/version.ts
2833
+ var PARSEMAN_VERSION = "0.32.0";
2834
+
2835
+ // src/analysis/gating.ts
2836
+ var isAny = (fs2) => fs2.kind === "any";
2837
+ function intersects2(a, b) {
2838
+ if (a.kind === "any" || b.kind === "any") return true;
2839
+ if (a.kind === "empty" || b.kind === "empty") return false;
2840
+ for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
2841
+ return false;
2842
+ }
2843
+ function intersection(a, b) {
2844
+ if (a.kind === "any") return b;
2845
+ if (b.kind === "any") return a;
2846
+ if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
2847
+ const ranges = [];
2848
+ for (const ra of a.ranges) for (const rb of b.ranges) {
2849
+ const lo = Math.max(ra.lo, rb.lo);
2850
+ const hi = Math.min(ra.hi, rb.hi);
2851
+ if (lo <= hi) ranges.push({ lo, hi });
2852
+ }
2853
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
2854
+ }
2855
+ function combine(sets) {
2856
+ let acc = { kind: "empty" };
2857
+ for (const s of sets) {
2858
+ if (s.kind === "any") return { kind: "any" };
2859
+ if (s.kind === "empty") continue;
2860
+ acc = acc.kind === "ranges" ? { kind: "ranges", ranges: [...acc.ranges, ...s.ranges] } : s;
2861
+ }
2862
+ return acc;
2863
+ }
2864
+ var ch = (c) => c >= 32 && c < 127 ? `'${String.fromCharCode(c)}'` : `\\u${c}`;
2865
+ function firstSetToString(fs2) {
2866
+ if (fs2.kind === "any") return "ANY";
2867
+ if (fs2.kind === "empty") return "(empty)";
2868
+ return fs2.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
2869
+ }
2870
+ var ruleNameOf = (p) => p._ruleName;
2871
+ var SUGGESTIONS = {
2872
+ "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.",
2873
+ "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.",
2874
+ "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.",
2875
+ "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.",
2876
+ "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2877
+ "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2878
+ };
2879
+ function classifyBroadArm(arm) {
2880
+ const seen = /* @__PURE__ */ new Set();
2881
+ const walk2 = (p) => {
2882
+ if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2883
+ seen.add(p);
2884
+ const d = p._def;
2885
+ switch (d.tag) {
2886
+ case "literal":
2887
+ case "regex":
2888
+ case "keywords":
2889
+ return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2890
+ case "not":
2891
+ return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
2892
+ case "scanTo":
2893
+ return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2894
+ // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
2895
+ // its own — it is the poison. `withCtx`/`recover` FORWARD their inner
2896
+ // first-set (see firstSetOf), so a broad result comes from the inner parser:
2897
+ // walk into it for a precise cause.
2898
+ case "guard":
2899
+ return { cause: "opaque-wrapper", detail: `opaque wrapper (${d.tag})` };
2900
+ // attempt/withCtx/recover FORWARD their inner first-set (transparent for
2901
+ // dispatch), so a broad result comes from the inner parser — walk into it.
2902
+ case "attempt":
2903
+ case "withCtx":
2904
+ case "recover":
2905
+ return walk2(d.parser);
2906
+ case "lazy": {
2907
+ const name = p._ruleName;
2908
+ try {
2909
+ const inner = walk2(d.thunk());
2910
+ if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
2911
+ return inner;
2912
+ } catch {
2913
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
2914
+ }
2915
+ }
2916
+ case "optional":
2917
+ case "many": {
2918
+ const inner = walk2(d.parser);
2919
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
2920
+ }
2921
+ case "oneOrMore":
2922
+ case "transform":
2923
+ case "label":
2924
+ case "field":
2925
+ case "trivia":
2926
+ case "token":
2927
+ case "leaf":
2928
+ case "node":
2929
+ case "grammar":
2930
+ case "expect":
2931
+ return walk2(d.parser);
2932
+ case "skip":
2933
+ return walk2(d.main);
2934
+ case "sequence": {
2935
+ let sawNullablePrefix = false;
2936
+ for (const t of d.parsers) {
2937
+ if (t._def.tag === "not") {
2938
+ sawNullablePrefix = true;
2939
+ continue;
2940
+ }
2941
+ if (isAny(firstSetOf(t))) {
2942
+ const inner = walk2(t);
2943
+ if (sawNullablePrefix)
2944
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
2945
+ return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2946
+ }
2947
+ if (matchesEmpty(t)) {
2948
+ sawNullablePrefix = true;
2949
+ continue;
2950
+ }
2951
+ return null;
2952
+ }
2953
+ return { cause: "broad-recognizer", detail: "sequence of only nullable/zero-width terms" };
2954
+ }
2955
+ case "choice": {
2956
+ for (const a of d.parsers) {
2957
+ const r = walk2(a);
2958
+ if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
2959
+ }
2960
+ return null;
2961
+ }
2962
+ default:
2963
+ return { cause: "broad-recognizer", detail: `unmodeled construct (${d.tag})` };
2964
+ }
2965
+ };
2966
+ return walk2(arm) ?? { cause: "broad-recognizer", detail: "broad (cause not localized)" };
2967
+ }
2968
+ function peelToLeading(arm) {
2969
+ let d = arm._def;
2970
+ let cur = arm;
2971
+ for (; ; ) {
2972
+ if (d.tag === "node" || d.tag === "grammar" || d.tag === "transform" || d.tag === "label" || d.tag === "attempt") {
2973
+ cur = d.parser;
2974
+ d = cur._def;
2975
+ continue;
2976
+ }
2977
+ break;
2978
+ }
2979
+ if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
2980
+ return cur;
2981
+ }
2982
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
2983
+ function detectAntiPatterns(rule, arms) {
2984
+ const out = [];
2985
+ arms.forEach((arm, i) => {
2986
+ const lead = peelToLeading(arm);
2987
+ const ld = lead._def;
2988
+ if (ld.tag === "not") {
2989
+ const inner = ld.parser;
2990
+ if (inner._def.tag === "not") {
2991
+ out.push({
2992
+ kind: "double-not",
2993
+ rule,
2994
+ armIndex: i,
2995
+ 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."
2996
+ });
2997
+ } else {
2998
+ out.push({
2999
+ kind: "leading-not",
3000
+ rule,
3001
+ armIndex: i,
3002
+ 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."
3003
+ });
3004
+ }
3005
+ }
3006
+ if (ld.tag === "regex" && KEYWORD_REGEX_RE.test(ld.source)) {
3007
+ out.push({
3008
+ kind: "keyword-regex",
3009
+ rule,
3010
+ armIndex: i,
3011
+ 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).`
3012
+ });
3013
+ }
3014
+ });
3015
+ return out;
3016
+ }
3017
+ function analyzeGating(entry, opts) {
3018
+ const raw = [];
3019
+ const antiPatterns = [];
3020
+ const visited = /* @__PURE__ */ new Set();
3021
+ const visit = (p, enclosingRule) => {
3022
+ if (visited.has(p)) return;
3023
+ visited.add(p);
3024
+ const d = p._def;
3025
+ const rule = ruleNameOf(p) ?? enclosingRule;
3026
+ if (d.tag === "choice") {
3027
+ raw.push({ g: analyzeChoice(p, d, rule), rule });
3028
+ antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3029
+ }
3030
+ const rec = d;
3031
+ const kids = [];
3032
+ if (Array.isArray(rec.parsers)) kids.push(...rec.parsers);
3033
+ for (const k of ["parser", "main", "skipped", "separator", "sentinel"])
3034
+ if (rec[k]) kids.push(rec[k]);
3035
+ if (Array.isArray(rec.skip)) kids.push(...rec.skip);
3036
+ if (d.tag === "lazy") {
3037
+ try {
3038
+ kids.push(d.thunk());
3039
+ } catch {
3040
+ }
3041
+ }
3042
+ for (const k of kids) visit(k, rule);
3043
+ };
3044
+ visit(entry, ruleNameOf(entry) ?? "<entry>");
3045
+ const perRule = /* @__PURE__ */ new Map();
3046
+ for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3047
+ const seenInRule = /* @__PURE__ */ new Map();
3048
+ const accept = new Set(opts?.accept ?? []);
3049
+ const usedAccept = /* @__PURE__ */ new Set();
3050
+ const choices = raw.map(({ g, rule }) => {
3051
+ const n = seenInRule.get(rule) ?? 0;
3052
+ seenInRule.set(rule, n + 1);
3053
+ const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
3054
+ const isAccepted = g.gates === "no" && accept.has(id);
3055
+ if (isAccepted) usedAccept.add(id);
3056
+ return { ...g, id, accepted: isAccepted };
3057
+ });
3058
+ const gated = choices.filter((c) => c.gates === "yes").length;
3059
+ const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3060
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3061
+ const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3062
+ const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3063
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3064
+ }
3065
+ function analyzeChoice(p, d, rule) {
3066
+ const arms = d.parsers;
3067
+ const shallow = arms.map((a) => a._meta.firstSet);
3068
+ const deep = arms.map((a) => firstSetOf(a));
3069
+ const anyArms = deep.map((fs2, index) => ({ fs: fs2, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3070
+ const { cause, detail } = classifyBroadArm(arms[index]);
3071
+ return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3072
+ });
3073
+ const overlaps = [];
3074
+ for (let i = 0; i < deep.length; i++)
3075
+ for (let j = i + 1; j < deep.length; j++)
3076
+ if (!isAny(deep[i]) && !isAny(deep[j]) && intersects2(deep[i], deep[j]))
3077
+ overlaps.push({
3078
+ a: i,
3079
+ b: j,
3080
+ on: intersection(deep[i], deep[j]),
3081
+ 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."
3082
+ });
3083
+ let gates;
3084
+ if (d.disjoint) gates = "yes";
3085
+ else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3086
+ else gates = "no";
3087
+ return {
3088
+ rule,
3089
+ strategy: d.strategy?.tag ?? "firstMatch",
3090
+ gates,
3091
+ combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3092
+ anyArms,
3093
+ overlaps
3094
+ };
3095
+ }
3096
+ function formatGatingWarnings(report) {
3097
+ const lines = [];
3098
+ for (const c of report.ungated) {
3099
+ lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
3100
+ for (const a of c.anyArms)
3101
+ lines.push(` \xB7 arm[${a.index}] first-set ANY (${a.cause}): ${a.detail}
3102
+ fix: ${a.suggestion}`);
3103
+ for (const o of c.overlaps)
3104
+ lines.push(` \xB7 arm[${o.a}] \u2229 arm[${o.b}] overlap on ${firstSetToString(o.on)}
3105
+ fix: ${o.suggestion}`);
3106
+ lines.push(` (intentional? accept it in the gating snapshot: { accept: ['${c.id}'] }.)`);
3107
+ }
3108
+ for (const ap of report.antiPatterns)
3109
+ lines.push(`parseman anti-pattern [${ap.kind}] @ ${ap.rule} arm[${ap.armIndex}]: ${ap.message}`);
3110
+ return lines;
3111
+ }
3112
+
3113
+ // src/combinators/scanTo.ts
3114
+ function resolveScanSkip(explicitSkip, raw, ctx) {
3115
+ if (raw) return [];
3116
+ const trivia2 = ctx.trivia;
3117
+ const ambient = ctx.scanSkip;
3118
+ if (!trivia2 && !ambient) return explicitSkip;
3119
+ const out = [];
3120
+ if (trivia2) out.push(trivia2);
3121
+ if (ambient) out.push(...ambient);
3122
+ out.push(...explicitSkip);
3123
+ return out;
3124
+ }
3125
+ function scanTo(sentinel, { skip: skip2, raw = false, orEOF = false } = {}) {
3126
+ const meta = {
3127
+ firstSet: any(),
3128
+ canMatchNewline: true,
3129
+ isTrivia: false
3130
+ };
3131
+ const explicitSkip = skip2 ?? [];
3132
+ return {
3133
+ _tag: "scanTo",
3134
+ _meta: meta,
3135
+ _def: { tag: "scanTo", sentinel, skip: explicitSkip, raw, orEOF },
3136
+ parse(input, pos, ctx) {
3137
+ let cur = pos;
3138
+ const skip3 = resolveScanSkip(explicitSkip, raw, ctx);
3139
+ const probeCtx = {
3140
+ trackLines: ctx.trackLines,
3141
+ state: ctx.state,
3142
+ ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
3143
+ };
3144
+ const emit2 = (end) => {
3145
+ if (end > pos && cstCaptureActive(ctx)) {
3146
+ const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
3147
+ pushCstLeaf(ctx, leaf2);
3148
+ }
3149
+ };
3150
+ while (cur < input.length) {
3151
+ const s = sentinel.parse(input, cur, probeCtx);
3152
+ if (s.ok) {
3153
+ emit2(cur);
3154
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3155
+ }
3156
+ let advanced = false;
3157
+ for (const skipper of skip3) {
3158
+ const r = skipper.parse(input, cur, probeCtx);
3159
+ if (r.ok && r.span.end > cur) {
3160
+ cur = r.span.end;
3161
+ advanced = true;
3162
+ break;
3163
+ }
3164
+ }
3165
+ if (!advanced) cur++;
3166
+ }
3167
+ if (orEOF) {
3168
+ emit2(cur);
3169
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3170
+ }
3171
+ const sentDef = sentinel._def;
3172
+ const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
3173
+ return { ok: false, expected, span: { start: pos, end: cur } };
3174
+ }
3175
+ };
3176
+ }
3177
+ function balanced(open, close, options = {}) {
3178
+ const ownSkip = options.skip ?? [];
3179
+ const combi = buildBalancedInterior(open, close, ownSkip);
3180
+ if (options.raw) return combi;
3181
+ const eagerParse = combi.parse.bind(combi);
3182
+ const cache = /* @__PURE__ */ new Map();
3183
+ combi._balancedAmbient = { open, close, ownSkip };
3184
+ combi.parse = (input, pos, ctx) => {
3185
+ const amb = ctx.scanSkip;
3186
+ if (!amb || amb.length === 0) return eagerParse(input, pos, ctx);
3187
+ let interior = cache.get(amb);
3188
+ if (!interior) {
3189
+ interior = buildBalancedInterior(open, close, [...amb, ...ownSkip]);
3190
+ cache.set(amb, interior);
3191
+ }
3192
+ return interior.parse(input, pos, ctx);
3193
+ };
3194
+ return combi;
3195
+ }
3196
+ function buildBalancedInterior(open, close, skips) {
3197
+ const self = ref();
3198
+ const stop = /* @__PURE__ */ new Set([open, close]);
3199
+ let bounded = true;
3200
+ for (const sk of skips) {
3201
+ const cs = firstSetClassChars(sk);
3202
+ if (cs === null) {
3203
+ bounded = false;
3204
+ break;
3205
+ }
3206
+ for (const ch2 of cs) stop.add(ch2);
3207
+ }
3208
+ const cls = [...stop].map(escapeClassChar).join("");
3209
+ const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
3210
+ const inner = many(choice(self, ...skips, content));
3211
+ const combi = transform(
3212
+ sequence(literal(open), inner, expect(literal(close))),
3213
+ // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
3214
+ // ParseError (recovered close). `c` is the close string or a ParseError.
3215
+ ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
3216
+ );
3217
+ if (combi._def.tag === "transform") {
3218
+ combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
3219
+ combi._def.recognitionOnly = true;
3220
+ }
3221
+ self.define(combi);
3222
+ return combi;
3223
+ }
3224
+ function firstSetClassChars(c) {
3225
+ const fs2 = c._meta.firstSet;
3226
+ if (fs2.kind === "empty") return [];
3227
+ if (fs2.kind !== "ranges") return null;
3228
+ const out = [];
3229
+ for (const { lo, hi } of fs2.ranges) {
3230
+ if (hi - lo > 8) return null;
3231
+ for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
3232
+ }
3233
+ return out;
3234
+ }
3235
+ function escapeClassChar(ch2) {
3236
+ return ch2.replace(/[\\\]^-]/g, "\\$&");
3237
+ }
3238
+
2800
3239
  // src/compiler/grammar-coverage-ids.ts
2801
3240
  function children(def, winners) {
2802
3241
  switch (def.tag) {
@@ -2909,16 +3348,16 @@ function literalCodePoints(frag) {
2909
3348
  const out = [];
2910
3349
  let i = 0;
2911
3350
  while (i < frag.length) {
2912
- const ch = frag[i];
2913
- if (ch === "\\") {
3351
+ const ch2 = frag[i];
3352
+ if (ch2 === "\\") {
2914
3353
  const e = frag[i + 1];
2915
3354
  if (e === void 0) return null;
2916
3355
  out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e] : e.codePointAt(0));
2917
3356
  i += 2;
2918
3357
  continue;
2919
3358
  }
2920
- if (META.has(ch)) return null;
2921
- out.push(ch.codePointAt(0));
3359
+ if (META.has(ch2)) return null;
3360
+ out.push(ch2.codePointAt(0));
2922
3361
  i += 1;
2923
3362
  }
2924
3363
  return out.length ? out : null;
@@ -2997,8 +3436,8 @@ function parseSeqParts(source) {
2997
3436
  };
2998
3437
  let i = 0;
2999
3438
  while (i < source.length) {
3000
- const ch = source[i];
3001
- if (ch === "[") {
3439
+ const ch2 = source[i];
3440
+ if (ch2 === "[") {
3002
3441
  let k = i + 1;
3003
3442
  const neg = source[k] === "^";
3004
3443
  if (neg) k++;
@@ -3020,7 +3459,7 @@ function parseSeqParts(source) {
3020
3459
  i += runFrom(ranges, neg, i);
3021
3460
  continue;
3022
3461
  }
3023
- if (ch === "\\") {
3462
+ if (ch2 === "\\") {
3024
3463
  const e = source[i + 1];
3025
3464
  if (e === "d" || e === "w" || e === "s") {
3026
3465
  flush();
@@ -3049,7 +3488,7 @@ function parseSeqParts(source) {
3049
3488
  } else lit.push(cp2);
3050
3489
  continue;
3051
3490
  }
3052
- if (ch === "(") {
3491
+ if (ch2 === "(") {
3053
3492
  if (source[i + 1] !== "?" || source[i + 2] !== ":") return null;
3054
3493
  let k = i + 3;
3055
3494
  let depth = 1;
@@ -3098,9 +3537,9 @@ function parseSeqParts(source) {
3098
3537
  }
3099
3538
  continue;
3100
3539
  }
3101
- if (META.has(ch)) return null;
3102
- const cp = ch.codePointAt(0);
3103
- i += ch.length;
3540
+ if (META.has(ch2)) return null;
3541
+ const cp = ch2.codePointAt(0);
3542
+ i += ch2.length;
3104
3543
  if (source[i] === "?") {
3105
3544
  flush();
3106
3545
  parts.push({ part: "lit", cps: [cp], optional: true });
@@ -3277,12 +3716,12 @@ function unwrapOuterGroup(source) {
3277
3716
  let i = 3;
3278
3717
  let ok = true;
3279
3718
  while (i < source.length && depth > 0) {
3280
- const ch = source[i];
3281
- if (ch === "\\") {
3719
+ const ch2 = source[i];
3720
+ if (ch2 === "\\") {
3282
3721
  i += 2;
3283
3722
  continue;
3284
3723
  }
3285
- if (ch === "[") {
3724
+ if (ch2 === "[") {
3286
3725
  let j = i + 1;
3287
3726
  if (source[j] === "^") j++;
3288
3727
  while (j < source.length && source[j] !== "]") {
@@ -3296,8 +3735,8 @@ function unwrapOuterGroup(source) {
3296
3735
  i = j + 1;
3297
3736
  continue;
3298
3737
  }
3299
- if (ch === "(") depth++;
3300
- else if (ch === ")") depth--;
3738
+ if (ch2 === "(") depth++;
3739
+ else if (ch2 === ")") depth--;
3301
3740
  i++;
3302
3741
  }
3303
3742
  if (!ok || depth !== 0 || i !== source.length) break;
@@ -3312,12 +3751,12 @@ function splitTopLevelAlternation(source) {
3312
3751
  let last = 0;
3313
3752
  let i = 0;
3314
3753
  while (i < body.length) {
3315
- const ch = body[i];
3316
- if (ch === "\\") {
3754
+ const ch2 = body[i];
3755
+ if (ch2 === "\\") {
3317
3756
  i += 2;
3318
3757
  continue;
3319
3758
  }
3320
- if (ch === "[") {
3759
+ if (ch2 === "[") {
3321
3760
  let j = i + 1;
3322
3761
  if (body[j] === "^") j++;
3323
3762
  while (j < body.length && body[j] !== "]") {
@@ -3328,18 +3767,18 @@ function splitTopLevelAlternation(source) {
3328
3767
  i = j + 1;
3329
3768
  continue;
3330
3769
  }
3331
- if (ch === "(") {
3770
+ if (ch2 === "(") {
3332
3771
  depth++;
3333
3772
  i++;
3334
3773
  continue;
3335
3774
  }
3336
- if (ch === ")") {
3775
+ if (ch2 === ")") {
3337
3776
  depth--;
3338
3777
  if (depth < 0) return null;
3339
3778
  i++;
3340
3779
  continue;
3341
3780
  }
3342
- if (ch === "|" && depth === 0) {
3781
+ if (ch2 === "|" && depth === 0) {
3343
3782
  parts.push(body.slice(last, i));
3344
3783
  last = i + 1;
3345
3784
  i++;
@@ -3458,12 +3897,12 @@ function splitDelimArms(body) {
3458
3897
  let last = 0;
3459
3898
  let i = 0;
3460
3899
  while (i < body.length) {
3461
- const ch = body[i];
3462
- if (ch === "\\") {
3900
+ const ch2 = body[i];
3901
+ if (ch2 === "\\") {
3463
3902
  i += 2;
3464
3903
  continue;
3465
3904
  }
3466
- if (ch === "[") {
3905
+ if (ch2 === "[") {
3467
3906
  let j = i + 1;
3468
3907
  if (body[j] === "^") j++;
3469
3908
  while (j < body.length && body[j] !== "]") {
@@ -3474,18 +3913,18 @@ function splitDelimArms(body) {
3474
3913
  i = j + 1;
3475
3914
  continue;
3476
3915
  }
3477
- if (ch === "(") {
3916
+ if (ch2 === "(") {
3478
3917
  depth++;
3479
3918
  i++;
3480
3919
  continue;
3481
3920
  }
3482
- if (ch === ")") {
3921
+ if (ch2 === ")") {
3483
3922
  depth--;
3484
3923
  if (depth < 0) return null;
3485
3924
  i++;
3486
3925
  continue;
3487
3926
  }
3488
- if (ch === "|" && depth === 0) {
3927
+ if (ch2 === "|" && depth === 0) {
3489
3928
  arms.push(body.slice(last, i));
3490
3929
  last = i + 1;
3491
3930
  }
@@ -3537,12 +3976,12 @@ function parseDelimited(source) {
3537
3976
  let k = i + 3;
3538
3977
  let depth = 1;
3539
3978
  while (k < source.length && depth > 0) {
3540
- const ch = source[k];
3541
- if (ch === "\\") {
3979
+ const ch2 = source[k];
3980
+ if (ch2 === "\\") {
3542
3981
  k += 2;
3543
3982
  continue;
3544
3983
  }
3545
- if (ch === "[") {
3984
+ if (ch2 === "[") {
3546
3985
  let j = k + 1;
3547
3986
  if (source[j] === "^") j++;
3548
3987
  while (j < source.length && source[j] !== "]") {
@@ -3553,8 +3992,8 @@ function parseDelimited(source) {
3553
3992
  k = j + 1;
3554
3993
  continue;
3555
3994
  }
3556
- if (ch === "(") depth++;
3557
- else if (ch === ")") depth--;
3995
+ if (ch2 === "(") depth++;
3996
+ else if (ch2 === ")") depth--;
3558
3997
  k++;
3559
3998
  }
3560
3999
  if (depth !== 0) return null;
@@ -4226,58 +4665,88 @@ function tryInlineDestructureTransform(src, valueVars) {
4226
4665
 
4227
4666
  // src/compiler/codegen.ts
4228
4667
  function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
4229
- if (seen.has(p)) return { concrete: empty(), refs: [] };
4230
- seen.add(p);
4231
4668
  const d = p._def;
4669
+ if (d.tag === "lazy") {
4670
+ const name = p._ruleName;
4671
+ if (name !== void 0) return { alts: [[{ set: empty(), nullable: false, ref: name }]] };
4672
+ }
4673
+ if (seen.has(p)) return { alts: [[]] };
4674
+ seen.add(p);
4232
4675
  const rec = (c) => leadingFirstSetRecipe(c, seen);
4233
- const merge = (a, b) => ({ concrete: union(a.concrete, b.concrete), refs: [...a.refs, ...b.refs] });
4234
- switch (d.tag) {
4235
- case "lazy": {
4236
- const name = p._ruleName;
4237
- if (name !== void 0) return { concrete: empty(), refs: [name] };
4238
- try {
4239
- return rec(d.thunk());
4240
- } catch {
4241
- return { concrete: any(), refs: [] };
4676
+ const seg = (set, nullable2) => ({ set, nullable: nullable2 });
4677
+ const forceNullable = (r) => ({ alts: r.alts.map((alt) => alt.map((s) => ({ ...s, nullable: true }))) });
4678
+ const collapse = (r, nullable2) => {
4679
+ let set = empty();
4680
+ let hasRef = false;
4681
+ for (const alt of r.alts) {
4682
+ for (const s of alt) {
4683
+ if (s.ref !== void 0) hasRef = true;
4684
+ set = union(set, s.set);
4685
+ if (!s.nullable) break;
4686
+ }
4687
+ }
4688
+ return seg(hasRef ? any() : set, nullable2);
4689
+ };
4690
+ const compute = () => {
4691
+ switch (d.tag) {
4692
+ case "lazy": {
4693
+ try {
4694
+ return rec(d.thunk());
4695
+ } catch {
4696
+ return { alts: [[seg(any(), false)]] };
4697
+ }
4242
4698
  }
4243
- }
4244
- case "literal":
4245
- case "regex":
4246
- case "keywords":
4247
- return { concrete: p._meta.firstSet, refs: [] };
4248
- case "choice": {
4249
- let out = { concrete: empty(), refs: [] };
4250
- for (const arm of d.parsers) out = merge(out, rec(arm));
4251
- return out;
4252
- }
4253
- case "sequence": {
4254
- let out = { concrete: empty(), refs: [] };
4255
- for (const term of d.parsers) {
4256
- out = merge(out, rec(term));
4257
- if (!matchesEmpty(term)) return out;
4699
+ case "literal":
4700
+ case "regex":
4701
+ case "keywords":
4702
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4703
+ case "choice": {
4704
+ const alts = [];
4705
+ for (const arm of d.parsers) alts.push(...rec(arm).alts);
4706
+ return { alts };
4707
+ }
4708
+ case "sequence": {
4709
+ const chain = [];
4710
+ for (const term of d.parsers) {
4711
+ if (!isZeroWidthAssertion(term)) {
4712
+ const tr = rec(term);
4713
+ if (tr.alts.length === 1) chain.push(...tr.alts[0]);
4714
+ else chain.push(collapse(tr, matchesEmpty(term)));
4715
+ }
4716
+ if (!matchesEmpty(term)) return { alts: [chain] };
4717
+ }
4718
+ return { alts: [chain] };
4258
4719
  }
4259
- return out;
4720
+ // `many`/`optional` are skippable as a whole → their leading chars start the
4721
+ // sequence but the chain must continue past them (force nullable). `oneOrMore`
4722
+ // requires at least one repetition, so it is NOT made nullable here.
4723
+ case "many":
4724
+ case "optional":
4725
+ return forceNullable(rec(d.parser));
4726
+ case "oneOrMore":
4727
+ case "transform":
4728
+ case "label":
4729
+ case "field":
4730
+ case "trivia":
4731
+ case "token":
4732
+ case "leaf":
4733
+ case "node":
4734
+ case "grammar":
4735
+ case "expect":
4736
+ return rec(d.parser);
4737
+ case "sepBy":
4738
+ return rec(d.parser);
4739
+ case "skip":
4740
+ return rec(d.main);
4741
+ // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4742
+ default:
4743
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4260
4744
  }
4261
- case "oneOrMore":
4262
- case "many":
4263
- case "optional":
4264
- case "transform":
4265
- case "label":
4266
- case "field":
4267
- case "trivia":
4268
- case "token":
4269
- case "leaf":
4270
- case "node":
4271
- case "grammar":
4272
- case "expect":
4273
- return rec(d.parser);
4274
- case "sepBy":
4275
- return rec(d.parser);
4276
- case "skip":
4277
- return rec(d.main);
4278
- // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4279
- default:
4280
- return { concrete: p._meta.firstSet, refs: [] };
4745
+ };
4746
+ try {
4747
+ return compute();
4748
+ } finally {
4749
+ seen.delete(p);
4281
4750
  }
4282
4751
  }
4283
4752
  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 }";
@@ -4420,9 +4889,9 @@ function emitLeafCapture(ctx, valExpr, startExpr, endExpr) {
4420
4889
  const i = ind(ctx);
4421
4890
  const lf = v(ctx, "_lf");
4422
4891
  return [
4423
- `${i}if (_ctx._cstLeaves) {`,
4892
+ `${i}if (_ctx._cstLeaves || _ctx._cstRawChildren) {`,
4424
4893
  `${i} const ${lf} = { _tag: 'leaf', value: ${valExpr}, span: { start: ${startExpr}, end: ${endExpr} } }`,
4425
- `${i} _ctx._cstLeaves.push(${lf})`,
4894
+ `${i} if (_ctx._cstLeaves) _ctx._cstLeaves.push(${lf})`,
4426
4895
  `${i} if (_ctx._cstRawChildren) _ctx._cstRawChildren.push(${lf})`,
4427
4896
  `${i}}`
4428
4897
  ];
@@ -4481,16 +4950,19 @@ function ensureTriviaFn(ctx) {
4481
4950
  const savedIndent = ctx.indent;
4482
4951
  const savedFailLabel = ctx.failLabel;
4483
4952
  const savedTrivia = ctx.activeTrivia;
4953
+ const savedScanSkip = ctx.activeScanSkip;
4484
4954
  const savedCapAsTrivia = ctx.capAsTrivia;
4485
4955
  ctx.indent = 2;
4486
4956
  ctx.failLabel = "_triv";
4487
4957
  ctx.capAsTrivia = true;
4488
4958
  ctx.activeTrivia = void 0;
4959
+ ctx.activeScanSkip = void 0;
4489
4960
  const r = emit(trivia2, ctx, "_pos");
4490
4961
  ctx.indent = savedIndent;
4491
4962
  ctx.failLabel = savedFailLabel;
4492
4963
  ctx.capAsTrivia = savedCapAsTrivia;
4493
4964
  ctx.activeTrivia = savedTrivia;
4965
+ ctx.activeScanSkip = savedScanSkip;
4494
4966
  ctx.namedFnDecls.push([
4495
4967
  `function ${fnName}(input, _pos, _ctx, _cap) {`,
4496
4968
  ` let _e = _pos`,
@@ -4768,7 +5240,7 @@ function emitKeywordsFast(def, ctx, pos) {
4768
5240
  const bodyInd = ind(ctx) + " ";
4769
5241
  const tries = [];
4770
5242
  for (const w of def.words) {
4771
- const cps = Array.from(w, (ch) => ch.codePointAt(0));
5243
+ const cps = Array.from(w, (ch2) => ch2.codePointAt(0));
4772
5244
  let shape = def.caseInsensitive ? { kind: "litFold", open: cps } : { kind: "seq", parts: [{ part: "lit", cps, optional: false }] };
4773
5245
  if (boundary) {
4774
5246
  shape = { kind: "lookahead", inner: shape, ranges: boundary.ranges, classNegated: boundary.negated, negative: true };
@@ -4961,7 +5433,7 @@ function canMatchEmptyAtStart(p) {
4961
5433
  const d = p._def;
4962
5434
  switch (d.tag) {
4963
5435
  case "regex":
4964
- return /(?:[*?]|\{0,|\{\d*,)/.test(d.source);
5436
+ return regexMatchesEmpty(d.source, d.flags);
4965
5437
  case "optional":
4966
5438
  return true;
4967
5439
  case "many":
@@ -5013,10 +5485,10 @@ function emitDisjointArm(p, ctx, pos, valV, endV, coverageId) {
5013
5485
  }
5014
5486
  function emitDisjointArmGated(def, i, ctx, pos, valV, endV, coverageId) {
5015
5487
  const p = def.parsers[i];
5016
- const gate = def.gates[i];
5017
- if (!gate) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
5488
+ const gate2 = def.gates[i];
5489
+ if (!gate2) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
5018
5490
  const gateIdx = ctx.mapFns.length;
5019
- ctx.mapFns.push(gate);
5491
+ ctx.mapFns.push(gate2);
5020
5492
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5021
5493
  const gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5022
5494
  const stmts = [`${ind(ctx)}if (${gateCond}) {`];
@@ -5174,7 +5646,7 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5174
5646
  if (preStmts) stmts.push(...preStmts);
5175
5647
  for (let i = 0; i < def.parsers.length; i++) {
5176
5648
  const p = def.parsers[i];
5177
- const gate = def.gates[i];
5649
+ const gate2 = def.gates[i];
5178
5650
  const autoNot = def.autoNot[i];
5179
5651
  const atStart = failsAtStart(p);
5180
5652
  const staticFx = armStaticExpected(ctx, p);
@@ -5186,9 +5658,9 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5186
5658
  })();
5187
5659
  const fsGuard = armGuardPrefix === void 0 ? baseFsGuard : baseFsGuard === null ? armGuardPrefix : `${armGuardPrefix} && (${baseFsGuard})`;
5188
5660
  let gateCond = null;
5189
- if (gate) {
5661
+ if (gate2) {
5190
5662
  const gateIdx = ctx.mapFns.length;
5191
- ctx.mapFns.push(gate);
5663
+ ctx.mapFns.push(gate2);
5192
5664
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5193
5665
  gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5194
5666
  }
@@ -5605,10 +6077,15 @@ function emitScanTo(def, ctx, pos) {
5605
6077
  const probeCtx = { ...ctx, capturing: false, noHoist: true };
5606
6078
  const { stmts: sentStmts, okVar: sentOk } = emitFallible(def.sentinel, probeCtx, curV);
5607
6079
  stmts.push(...sentStmts, `${ind(ctx)}if (${sentOk}) { ${foundV} = true; break }`);
5608
- if (def.skip.length > 0) {
6080
+ const effectiveSkip = def.raw ? def.skip : [
6081
+ ...ctx.activeTrivia ? [ctx.activeTrivia] : [],
6082
+ ...ctx.activeScanSkip ?? [],
6083
+ ...def.skip
6084
+ ];
6085
+ if (effectiveSkip.length > 0) {
5609
6086
  const advV = v(ctx, "_stadv");
5610
6087
  stmts.push(`${ind(ctx)}let ${advV} = false`);
5611
- for (const skipper of def.skip) {
6088
+ for (const skipper of effectiveSkip) {
5612
6089
  const { stmts: skStmts, okVar: skOk, endVar: skEnd } = emitFallible(skipper, probeCtx, curV);
5613
6090
  stmts.push(
5614
6091
  `${ind(ctx)}if (!${advV}) {`,
@@ -5826,8 +6303,10 @@ function emitNode(def, ctx, pos) {
5826
6303
  const directCstV = !structural && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
5827
6304
  const directCstGate = directCstV === null ? "false" : `${profileCapture} || _ctx.build?._parsemanCstOutput === true`;
5828
6305
  const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
6306
+ const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
6307
+ const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
5829
6308
  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)))` : ""}
5830
- ${i}const ${chV} = ${profileRecognizer} ? undefined : [], ${rawV} = ${profileRecognizer} ? undefined : [], ${tlV} = ${profileRecognizer} ? undefined : ${innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`}` : capturesTrivia ? `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
6309
+ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [], ${tlV} = ${profileRecognizer} ? undefined : ${innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`}` : capturesTrivia ? `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
5831
6310
  ` : ""}${i}const ${capTLv} = !(${profileRecognizer}), ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}, ${tlV} = ${profileRecognizer} ? undefined : []` : `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
5832
6311
  ` : ""}${i}const ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}`;
5833
6312
  const innerTl = structural || capturesTrivia ? structural && !innerEnablesTriviaCapture ? `${capTLv} ? ${tlV} : undefined` : tlV : "undefined";
@@ -6076,6 +6555,24 @@ function emit(p, ctx, pos) {
6076
6555
  return instrument(dispatch());
6077
6556
  }
6078
6557
  function emitDispatch(p, ctx, pos) {
6558
+ const bal = p._balancedAmbient;
6559
+ if (bal && ctx.activeScanSkip && ctx.activeScanSkip.length > 0 && !ctx.balancedRebuildStack?.has(p)) {
6560
+ const interior = buildBalancedInterior(bal.open, bal.close, [...ctx.activeScanSkip, ...bal.ownSkip]);
6561
+ markUnusedValues(interior);
6562
+ if (ctx.lazyUsage) {
6563
+ const u = analyzeLazyUsage(interior);
6564
+ for (const [k, n] of u.counts) ctx.lazyUsage.counts.set(k, (ctx.lazyUsage.counts.get(k) ?? 0) + n);
6565
+ for (const k of u.recursive) ctx.lazyUsage.recursive.add(k);
6566
+ for (const [k, s] of u.sizes) ctx.lazyUsage.sizes.set(k, s);
6567
+ }
6568
+ const stack = ctx.balancedRebuildStack ?? (ctx.balancedRebuildStack = /* @__PURE__ */ new Set());
6569
+ stack.add(p);
6570
+ try {
6571
+ return emit(interior, ctx, pos);
6572
+ } finally {
6573
+ stack.delete(p);
6574
+ }
6575
+ }
6079
6576
  const def = p._def;
6080
6577
  switch (def.tag) {
6081
6578
  case "literal":
@@ -6497,9 +6994,36 @@ function ruleDependencies(ruleMap) {
6497
6994
  }
6498
6995
  return deps;
6499
6996
  }
6997
+ function resolveGatingLevel(opt) {
6998
+ const explicit = typeof opt === "string" ? opt : opt?.level;
6999
+ if (explicit !== void 0) return explicit;
7000
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_GATING : void 0;
7001
+ if (env === "off" || env === "warn" || env === "error") return env;
7002
+ return "warn";
7003
+ }
7004
+ function runGatingDiagnostic(combinator, opt) {
7005
+ const level = resolveGatingLevel(opt);
7006
+ if (level === "off") return void 0;
7007
+ const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7008
+ let report;
7009
+ try {
7010
+ report = analyzeGating(combinator, accept ? { accept } : void 0);
7011
+ } catch {
7012
+ return void 0;
7013
+ }
7014
+ const lines = formatGatingWarnings(report);
7015
+ if (lines.length > 0) {
7016
+ if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
7017
+ ${lines.join("\n")}`);
7018
+ for (const l of lines) console.warn(l);
7019
+ }
7020
+ return report;
7021
+ }
6500
7022
  function compile(combinator, mapFnSources, opts) {
7023
+ const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
6501
7024
  markUnusedValues(combinator);
6502
7025
  const grammarTrivia = combinator._meta.grammarTrivia;
7026
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
6503
7027
  const ctx = {
6504
7028
  vars: 0,
6505
7029
  indent: 1,
@@ -6521,7 +7045,8 @@ function compile(combinator, mapFnSources, opts) {
6521
7045
  recovery: opts?.recovery ?? false,
6522
7046
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
6523
7047
  lazyUsage: analyzeLazyUsage(combinator),
6524
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7048
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7049
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6525
7050
  };
6526
7051
  const r = emit(combinator, ctx, "_pos");
6527
7052
  const coverageRootRuleId = ctx.coverage?.plan.rules.get(combinator);
@@ -6585,6 +7110,7 @@ function compile(combinator, mapFnSources, opts) {
6585
7110
  source,
6586
7111
  inlineExpression,
6587
7112
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
7113
+ ...gatingReport === void 0 ? {} : { gating: gatingReport },
6588
7114
  parse(input, pos = 0) {
6589
7115
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
6590
7116
  },
@@ -6620,6 +7146,7 @@ function compileRuleMap(ruleMap, opts) {
6620
7146
  for (const [, rule] of ruleMap) markUnusedValues(rule);
6621
7147
  const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
6622
7148
  const grammarTrivia = opts?.trivia;
7149
+ const grammarScanSkip = opts?.scanSkip ?? ruleMap.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6623
7150
  const ctx = {
6624
7151
  vars: 0,
6625
7152
  indent: 1,
@@ -6641,7 +7168,8 @@ function compileRuleMap(ruleMap, opts) {
6641
7168
  recovery: opts?.recovery ?? false,
6642
7169
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(ruleMap.map(([, rule]) => rule), coverageWinners) } } : {},
6643
7170
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6644
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7171
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7172
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6645
7173
  };
6646
7174
  const seenNames = /* @__PURE__ */ new Map();
6647
7175
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6655,9 +7183,14 @@ function compileRuleMap(ruleMap, opts) {
6655
7183
  ctx.ruleNames = ruleNames;
6656
7184
  const perEntry = ruleMap.map(([key, rule]) => {
6657
7185
  const savedTrivia = ctx.activeTrivia;
6658
- if (rule._meta.isTrivia) ctx.activeTrivia = void 0;
7186
+ const savedScanSkip = ctx.activeScanSkip;
7187
+ if (rule._meta.isTrivia) {
7188
+ ctx.activeTrivia = void 0;
7189
+ ctx.activeScanSkip = void 0;
7190
+ }
6659
7191
  const r = emit(rule, ctx, "_pos");
6660
7192
  ctx.activeTrivia = savedTrivia;
7193
+ ctx.activeScanSkip = savedScanSkip;
6661
7194
  return { key, rule, r };
6662
7195
  });
6663
7196
  const derivedSrcs = ctx.mapFnSrcs.length === ctx.mapFns.length && ctx.mapFnSrcs.every((s) => s !== null) ? ctx.mapFnSrcs : void 0;
@@ -6730,6 +7263,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6730
7263
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
6731
7264
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
6732
7265
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7266
+ const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6733
7267
  const ruleMap = ruleMapArg.filter(([, val]) => {
6734
7268
  const d = val._def;
6735
7269
  if (d.tag !== "lazy") return true;
@@ -6767,7 +7301,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
6767
7301
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6768
7302
  ns,
6769
7303
  deferFirstSetRefs: true,
6770
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7304
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7305
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6771
7306
  };
6772
7307
  const seen = /* @__PURE__ */ new Map();
6773
7308
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6831,9 +7366,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
6831
7366
  ctx.failLabel = "_pfail";
6832
7367
  ctx.recordFail = true;
6833
7368
  const savedTrivia = ctx.activeTrivia;
6834
- if (rule._meta.isTrivia) ctx.activeTrivia = void 0;
7369
+ const savedScanSkip = ctx.activeScanSkip;
7370
+ if (rule._meta.isTrivia) {
7371
+ ctx.activeTrivia = void 0;
7372
+ ctx.activeScanSkip = void 0;
7373
+ }
6835
7374
  const body = emit(resolved, ctx, "_pos");
6836
7375
  ctx.activeTrivia = savedTrivia;
7376
+ ctx.activeScanSkip = savedScanSkip;
6837
7377
  ctx.indent = savedIndent;
6838
7378
  ctx.failLabel = savedFail;
6839
7379
  ctx.recordFail = savedRec;
@@ -6880,6 +7420,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6880
7420
  }
6881
7421
  const firstSets = /* @__PURE__ */ new Map();
6882
7422
  const firstSetRecipes = /* @__PURE__ */ new Map();
7423
+ const nullable2 = /* @__PURE__ */ new Map();
6883
7424
  for (const [key, rule] of ruleMap) {
6884
7425
  let resolved = rule;
6885
7426
  const d = rule._def;
@@ -6891,15 +7432,18 @@ function compileLinkable(ruleMapArg, ns, opts) {
6891
7432
  }
6892
7433
  }
6893
7434
  firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
6894
- firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { concrete: any(), refs: [] } : leadingFirstSetRecipe(resolved));
7435
+ nullable2.set(key, matchesEmpty(resolved));
7436
+ firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { alts: [[{ set: any(), nullable: false }]] } : leadingFirstSetRecipe(resolved));
6895
7437
  }
6896
7438
  return {
7439
+ v: PARSEMAN_VERSION,
6897
7440
  ns,
6898
7441
  keys: perEntry.map((e) => e.key),
6899
7442
  prelude,
6900
7443
  ruleFns,
6901
7444
  wrappers,
6902
7445
  firstSets,
7446
+ nullable: nullable2,
6903
7447
  firstSetRecipes,
6904
7448
  deps: ruleDependencies(ruleMap),
6905
7449
  needsEmptyTl: !!ctx.needsEmptyTl,
@@ -6978,101 +7522,6 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
6978
7522
  ].join("\n");
6979
7523
  }
6980
7524
 
6981
- // src/combinators/scanTo.ts
6982
- function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
6983
- const meta = {
6984
- firstSet: any(),
6985
- canMatchNewline: true,
6986
- isTrivia: false
6987
- };
6988
- return {
6989
- _tag: "scanTo",
6990
- _meta: meta,
6991
- _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
6992
- parse(input, pos, ctx) {
6993
- let cur = pos;
6994
- const probeCtx = {
6995
- trackLines: ctx.trackLines,
6996
- state: ctx.state,
6997
- ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
6998
- };
6999
- const emit2 = (end) => {
7000
- if (end > pos && cstCaptureActive(ctx)) {
7001
- const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
7002
- pushCstLeaf(ctx, leaf2);
7003
- }
7004
- };
7005
- while (cur < input.length) {
7006
- const s = sentinel.parse(input, cur, probeCtx);
7007
- if (s.ok) {
7008
- emit2(cur);
7009
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
7010
- }
7011
- let advanced = false;
7012
- for (const skipper of skip2) {
7013
- const r = skipper.parse(input, cur, probeCtx);
7014
- if (r.ok && r.span.end > cur) {
7015
- cur = r.span.end;
7016
- advanced = true;
7017
- break;
7018
- }
7019
- }
7020
- if (!advanced) cur++;
7021
- }
7022
- if (orEOF) {
7023
- emit2(cur);
7024
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
7025
- }
7026
- const sentDef = sentinel._def;
7027
- const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
7028
- return { ok: false, expected, span: { start: pos, end: cur } };
7029
- }
7030
- };
7031
- }
7032
- function balanced(open, close, options = {}) {
7033
- const self = ref();
7034
- const skips = options.skip ?? [];
7035
- const stop = /* @__PURE__ */ new Set([open, close]);
7036
- let bounded = true;
7037
- for (const sk of skips) {
7038
- const cs = firstSetClassChars(sk);
7039
- if (cs === null) {
7040
- bounded = false;
7041
- break;
7042
- }
7043
- for (const ch of cs) stop.add(ch);
7044
- }
7045
- const cls = [...stop].map(escapeClassChar).join("");
7046
- const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
7047
- const inner = many(choice(self, ...skips, content));
7048
- const combi = transform(
7049
- sequence(literal(open), inner, expect(literal(close))),
7050
- // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
7051
- // ParseError (recovered close). `c` is the close string or a ParseError.
7052
- ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
7053
- );
7054
- if (combi._def.tag === "transform") {
7055
- combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
7056
- combi._def.recognitionOnly = true;
7057
- }
7058
- self.define(combi);
7059
- return combi;
7060
- }
7061
- function firstSetClassChars(c) {
7062
- const fs2 = c._meta.firstSet;
7063
- if (fs2.kind === "empty") return [];
7064
- if (fs2.kind !== "ranges") return null;
7065
- const out = [];
7066
- for (const { lo, hi } of fs2.ranges) {
7067
- if (hi - lo > 8) return null;
7068
- for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
7069
- }
7070
- return out;
7071
- }
7072
- function escapeClassChar(ch) {
7073
- return ch.replace(/[\\\]^-]/g, "\\$&");
7074
- }
7075
-
7076
7525
  // src/combinators/withCtx.ts
7077
7526
  function withCtx(extra, combinator) {
7078
7527
  const meta = {
@@ -7129,7 +7578,7 @@ function childrenOf2(def) {
7129
7578
  return [];
7130
7579
  }
7131
7580
  }
7132
- var ruleNameOf = (c) => c._ruleName;
7581
+ var ruleNameOf2 = (c) => c._ruleName;
7133
7582
  function lazyTarget(c) {
7134
7583
  if (c._def.tag !== "lazy") return null;
7135
7584
  try {
@@ -7196,15 +7645,15 @@ function evalRuleMapIR(ir) {
7196
7645
  if (Array.isArray(it)) {
7197
7646
  const [src, comb] = it;
7198
7647
  gateSrcs.push(src);
7199
- let gate;
7648
+ let gate2;
7200
7649
  try {
7201
- gate = (0, eval)(`(${src})`);
7650
+ gate2 = (0, eval)(`(${src})`);
7202
7651
  } catch {
7203
- gate = () => {
7652
+ gate2 = () => {
7204
7653
  throw new Error("IR gate fn not materialized");
7205
7654
  };
7206
7655
  }
7207
- return { gate, combinator: comb };
7656
+ return { gate: gate2, combinator: comb };
7208
7657
  }
7209
7658
  gateSrcs.push(null);
7210
7659
  return it;
@@ -7311,7 +7760,7 @@ var Serializer = class {
7311
7760
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7312
7761
  * `g[name]` ref — only the top-level entry is unwrapped. */
7313
7762
  body(c) {
7314
- return c._def.tag === "lazy" && ruleNameOf(c) !== void 0 ? lazyTarget(c) ?? c : c;
7763
+ return c._def.tag === "lazy" && ruleNameOf2(c) !== void 0 ? lazyTarget(c) ?? c : c;
7315
7764
  }
7316
7765
  run() {
7317
7766
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
@@ -7336,7 +7785,7 @@ ${body}
7336
7785
  analyze(c, active) {
7337
7786
  const def = c._def;
7338
7787
  if (def.tag === "lazy") {
7339
- if (ruleNameOf(c) !== void 0) return;
7788
+ if (ruleNameOf2(c) !== void 0) return;
7340
7789
  const target = lazyTarget(c);
7341
7790
  if (!target) throw new Unserializable("unresolved unnamed ref");
7342
7791
  if (active.has(target)) {
@@ -7388,7 +7837,7 @@ ${body}
7388
7837
  const kid = (x) => this.ref(x, selfOf);
7389
7838
  switch (def.tag) {
7390
7839
  case "lazy": {
7391
- if (ruleNameOf(c) !== void 0) return `g[${JSON.stringify(ruleNameOf(c))}]`;
7840
+ if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
7392
7841
  const target = lazyTarget(c);
7393
7842
  if (selfOf && target === selfOf.comb) return selfOf.var;
7394
7843
  return this.ref(target, selfOf);
@@ -7442,7 +7891,7 @@ ${body}
7442
7891
  case "skip":
7443
7892
  return `skip(${kid(def.main)}, ${kid(def.skipped)})`;
7444
7893
  case "scanTo":
7445
- return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}], orEOF: ${def.orEOF} })`;
7894
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}]${def.raw ? ", raw: true" : ""}, orEOF: ${def.orEOF} })`;
7446
7895
  case "transform": {
7447
7896
  if (def.fnSrc === void 0) throw new Unserializable("transform without fnSrc");
7448
7897
  return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ", true" : ""})`;
@@ -7499,7 +7948,7 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
7499
7948
  return buildCstNode(typeOrOptions, children2 ?? [], _fields, span ?? { start: 0, end: 0 }, rawChildren, triviaLog, state);
7500
7949
  }
7501
7950
  const collapse = normalizeCstCollapse(typeOrOptions?.collapse);
7502
- const host = (type, children3, fields, span2, rawChildren2, triviaLog2, state2) => buildCstNode(type, children3, fields, span2, rawChildren2, triviaLog2, state2);
7951
+ const host = (type, children3, fields, span2, rawChildren2, triviaLog2, state2) => buildCstNode(type, children3 ?? [], fields, span2, rawChildren2, triviaLog2, state2);
7503
7952
  host._parsemanCstOutput = true;
7504
7953
  if (collapse) host._parsemanCstCollapse = collapse;
7505
7954
  return host;
@@ -7523,6 +7972,18 @@ function pickPieces(pieces, names) {
7523
7972
  return pieces.map((pc) => ({ ...pc, keys: pc.keys.filter((k) => keep.has(k) && winner.get(k) === pc), ruleFns: filt(pc.ruleFns, pc), wrappers: filt(pc.wrappers, pc), deps: filt(pc.deps, pc) })).filter((pc) => pc.keys.length > 0);
7524
7973
  }
7525
7974
  function fusedBody(pieces) {
7975
+ for (const p of pieces) {
7976
+ if (p.v === void 0) {
7977
+ throw new Error(
7978
+ `parseman: artifact "${p.ns}" is UNSTAMPED (compiled before the version-lock invariant). Recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse unversioned or cross-version artifacts.`
7979
+ );
7980
+ }
7981
+ if (p.v !== PARSEMAN_VERSION) {
7982
+ throw new Error(
7983
+ `parseman: artifact "${p.ns}" was compiled with parseman ${p.v}, but is being fused with parseman ${PARSEMAN_VERSION}. Compiled grammar artifacts are version-locked \u2014 recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse across versions.`
7984
+ );
7985
+ }
7986
+ }
7526
7987
  const winner = /* @__PURE__ */ new Map();
7527
7988
  for (const p of pieces) for (const k of p.keys) winner.set(k, p);
7528
7989
  for (const [k, p] of winner) {
@@ -7548,23 +8009,38 @@ function fusedBody(pieces) {
7548
8009
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
7549
8010
  const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
7550
8011
  const ANY2 = { kind: "any" };
8012
+ const EMPTY3 = { kind: "empty" };
7551
8013
  const recipes = /* @__PURE__ */ new Map();
7552
8014
  const shallow = /* @__PURE__ */ new Map();
8015
+ const nullable2 = /* @__PURE__ */ new Map();
7553
8016
  for (const [k, p] of winner) {
7554
8017
  const r = p.firstSetRecipes?.get(k);
7555
8018
  if (r) recipes.set(k, r);
7556
8019
  const fs2 = p.firstSets?.get(k);
7557
8020
  if (fs2) shallow.set(k, fs2);
8021
+ const nu = p.nullable?.get(k);
8022
+ if (nu !== void 0) nullable2.set(k, nu);
7558
8023
  }
8024
+ const knownRef = (ref2) => recipes.has(ref2) || shallow.has(ref2);
8025
+ const refFS = (ref2) => knownRef(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2;
8026
+ const refNullable = (ref2) => knownRef(ref2) ? nullable2.get(ref2) ?? true : true;
8027
+ const resolveRecipe = (r) => {
8028
+ let out = EMPTY3;
8029
+ for (const chain of r.alts) {
8030
+ for (const seg of chain) {
8031
+ out = union(out, seg.ref !== void 0 ? refFS(seg.ref) : seg.set);
8032
+ const skippable = seg.ref !== void 0 ? seg.nullable || refNullable(seg.ref) : seg.nullable;
8033
+ if (!skippable) break;
8034
+ }
8035
+ }
8036
+ return out;
8037
+ };
7559
8038
  const finalFS = /* @__PURE__ */ new Map();
7560
- for (const [k] of winner) finalFS.set(k, recipes.get(k)?.concrete ?? shallow.get(k) ?? ANY2);
8039
+ for (const [k] of winner) finalFS.set(k, recipes.has(k) ? EMPTY3 : shallow.get(k) ?? ANY2);
7561
8040
  for (let iter = 0; iter <= recipes.size; iter++) {
7562
8041
  let changed = false;
7563
8042
  for (const [k, r] of recipes) {
7564
- let fs2 = r.concrete;
7565
- for (const ref2 of r.refs) {
7566
- fs2 = union(fs2, recipes.has(ref2) || shallow.has(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2);
7567
- }
8043
+ const fs2 = resolveRecipe(r);
7568
8044
  if (JSON.stringify(finalFS.get(k)) !== JSON.stringify(fs2)) {
7569
8045
  finalFS.set(k, fs2);
7570
8046
  changed = true;
@@ -7612,8 +8088,8 @@ function materializePiece(p, trivia2, captureTerminals = false) {
7612
8088
  return pieces;
7613
8089
  }
7614
8090
 
7615
- // src/combinators/guard.ts
7616
- function guard(predicate) {
8091
+ // src/combinators/gate.ts
8092
+ function gate(predicate) {
7617
8093
  const meta = {
7618
8094
  firstSet: { kind: "any" },
7619
8095
  canMatchNewline: false,
@@ -7870,17 +8346,17 @@ function isGatedArmExpr(e) {
7870
8346
  return hasGate && hasCombinator;
7871
8347
  }
7872
8348
  function gatedArmParts(e) {
7873
- let gate;
8349
+ let gate2;
7874
8350
  let combinator;
7875
8351
  for (const prop of e.properties) {
7876
8352
  if (prop.type !== "Property") return null;
7877
8353
  const op = prop;
7878
8354
  const name = propName(op);
7879
- if (name === "gate") gate = op.value;
8355
+ if (name === "gate") gate2 = op.value;
7880
8356
  else if (name === "combinator") combinator = op.value;
7881
8357
  else return null;
7882
8358
  }
7883
- return gate && combinator ? { gate, combinator } : null;
8359
+ return gate2 && combinator ? { gate: gate2, combinator } : null;
7884
8360
  }
7885
8361
  function isCombinator(v2) {
7886
8362
  return !!v2 && typeof v2 === "object" && "_def" in v2;
@@ -8082,14 +8558,14 @@ function exprToCombi(node2, scope, code, mfs) {
8082
8558
  return null;
8083
8559
  }
8084
8560
  }
8085
- if (callee.name === "guard") {
8561
+ if (callee.name === "gate" || callee.name === "guard") {
8086
8562
  if (code === void 0 || mfs === void 0) return null;
8087
8563
  const [predArg] = node2.arguments;
8088
8564
  if (!predArg || predArg.type === "SpreadElement") return null;
8089
8565
  const predSrc = stripTsFromSource(predArg, code);
8090
8566
  mfs.push(predSrc);
8091
8567
  try {
8092
- const combi = guard(() => true);
8568
+ const combi = gate(() => true);
8093
8569
  if (combi._def.tag !== "guard") return null;
8094
8570
  combi._def.predSrc = predSrc;
8095
8571
  return combi;
@@ -8713,21 +9189,24 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8713
9189
  return null;
8714
9190
  }
8715
9191
  const optionsArg = optionsFirst ? arg0 : arg1;
8716
- const triviaValue = optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
8717
- (p) => p.key?.name === "trivia"
9192
+ const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
9193
+ (p) => p.key?.name === name
8718
9194
  )?.value : void 0;
9195
+ const triviaValue = optionValue("trivia");
8719
9196
  const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
8720
- return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {} };
9197
+ const scanSkipValue = optionValue("scanSkip");
9198
+ const gScanSkip = scanSkipValue ? evaluateCombinatorArray(scanSkipValue, scope, code) ?? void 0 : void 0;
9199
+ return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
8721
9200
  };
8722
9201
  const compileRulesFactory = (init, label2) => {
8723
9202
  const evaluated = evaluateRulesFactory(init, label2);
8724
9203
  if (!evaluated) return null;
8725
- const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, recovery, coverage: grammarCoverage });
9204
+ const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
8726
9205
  if (!compiled) {
8727
9206
  warn(init.start, `${label2}: rule map couldn't be inlined`);
8728
9207
  return null;
8729
9208
  }
8730
- return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {} };
9209
+ return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
8731
9210
  };
8732
9211
  const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
8733
9212
  const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
@@ -8735,13 +9214,14 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8735
9214
  const isPickCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "pick";
8736
9215
  const localRuleMaps = /* @__PURE__ */ new Map();
8737
9216
  const localGrammarTrivia = /* @__PURE__ */ new Map();
9217
+ const localGrammarScanSkip = /* @__PURE__ */ new Map();
8738
9218
  const nsFor = (label2) => `_${(0, import_node_crypto.createHash)("sha1").update(`${id}#${label2}`).digest("hex").slice(0, 8)}_`;
8739
9219
  const stripIndent = (s) => s.replace(/\n[ \t]+/g, "\n");
8740
9220
  const serializePieces = (p) => {
8741
9221
  const mapLit = (m, stripVals = false) => `new Map([${[...m].map(
8742
9222
  ([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(stripVals && typeof v2 === "string" ? stripIndent(v2) : v2)}]`
8743
9223
  ).join(", ")}])`;
8744
- return `{ ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
9224
+ return `{ v: ${JSON.stringify(p.v)}, ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, nullable: ${p.nullable ? mapLit(p.nullable) : "new Map()"}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
8745
9225
  };
8746
9226
  const isSpread = (it) => "__spreadLocal" in it;
8747
9227
  const isIR = (it) => "ir" in it && !("ruleFns" in it);
@@ -8981,11 +9461,15 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8981
9461
  }
8982
9462
  const localArg = elements[elements.length - 1];
8983
9463
  let localRules = null;
9464
+ let localScanSkip;
8984
9465
  if (isRulesCall(localArg)) {
8985
9466
  const evaluated = evaluateRulesFactory(localArg, `composeLeaf${init.start}`);
8986
9467
  localRules = evaluated?.ruleMap ?? null;
9468
+ localScanSkip = evaluated?.scanSkip;
8987
9469
  } else if (localArg.type === "Identifier") {
8988
- localRules = localRuleMaps.get(localArg.name) ?? null;
9470
+ const name = localArg.name;
9471
+ localRules = localRuleMaps.get(name) ?? null;
9472
+ localScanSkip = localGrammarScanSkip.get(name);
8989
9473
  }
8990
9474
  if (!localRules) {
8991
9475
  warn(init.start, "composeLeaf(): final argument must be a local rules() map");
@@ -9003,7 +9487,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9003
9487
  }
9004
9488
  try {
9005
9489
  const localNs = nsFor(`composeLeaf${init.start}`);
9006
- const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, recovery });
9490
+ const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
9007
9491
  if (!plainLocalPiece) {
9008
9492
  warn(init.start, "composeLeaf(): local rules could not be statically compiled");
9009
9493
  return null;
@@ -9105,6 +9589,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9105
9589
  if (!compiledRules) continue;
9106
9590
  localRuleMaps.set(varName, compiledRules.ruleMap);
9107
9591
  if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia);
9592
+ if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip);
9108
9593
  let replacement = withCoverageDefinitions(
9109
9594
  compiledRules.replacement,
9110
9595
  compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(compiledRules.replacement)
@@ -9233,9 +9718,18 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9233
9718
  ms.overwrite(imp.start, imp.end, stripped);
9234
9719
  }
9235
9720
  }
9236
- for (const { start, end, replacement } of (runtimeComposeFallback ? [] : replacements).sort((a, b) => b.start - a.start)) {
9721
+ const applied = runtimeComposeFallback ? [] : replacements;
9722
+ for (const { start, end, replacement } of applied.slice().sort((a, b) => b.start - a.start)) {
9237
9723
  ms.overwrite(start, end, replacement);
9238
9724
  }
9725
+ if (applied.length > 0) {
9726
+ ms.prepend(
9727
+ `// Generated by parseman v${PARSEMAN_VERSION} \u2014 DO NOT EDIT.
9728
+ // Version-locked: compile AND fuse/link this artifact with parseman v${PARSEMAN_VERSION} ONLY.
9729
+ // Parseman does not read artifacts across versions; recompile if the version differs.
9730
+ `
9731
+ );
9732
+ }
9239
9733
  const unlowered = endLoweringCapture();
9240
9734
  if (warnUnloweredRegex) {
9241
9735
  for (const src of unlowered) {