parseman 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/analysis/gating.d.ts +115 -0
  2. package/dist/analysis/gating.d.ts.map +1 -0
  3. package/dist/combinators/first-set.d.ts +25 -0
  4. package/dist/combinators/first-set.d.ts.map +1 -1
  5. package/dist/combinators/gate.d.ts +24 -0
  6. package/dist/combinators/gate.d.ts.map +1 -0
  7. package/dist/combinators/grammar.d.ts.map +1 -1
  8. package/dist/combinators/guard.d.ts +5 -12
  9. package/dist/combinators/guard.d.ts.map +1 -1
  10. package/dist/combinators/parser.d.ts +7 -0
  11. package/dist/combinators/parser.d.ts.map +1 -1
  12. package/dist/combinators/regex.d.ts.map +1 -1
  13. package/dist/combinators/scanTo.d.ts +45 -3
  14. package/dist/combinators/scanTo.d.ts.map +1 -1
  15. package/dist/compiler/codegen.d.ts +78 -14
  16. package/dist/compiler/codegen.d.ts.map +1 -1
  17. package/dist/compiler/ir-serialize.d.ts +1 -1
  18. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  19. package/dist/compiler/linker.d.ts.map +1 -1
  20. package/dist/functional/run.d.ts.map +1 -1
  21. package/dist/index.cjs +760 -246
  22. package/dist/index.cjs.map +4 -4
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +760 -246
  26. package/dist/index.js.map +4 -4
  27. package/dist/language-service/index.cjs +2 -0
  28. package/dist/language-service/index.cjs.map +2 -2
  29. package/dist/language-service/index.js +2 -0
  30. package/dist/language-service/index.js.map +2 -2
  31. package/dist/plugin/index.cjs +803 -270
  32. package/dist/plugin/index.cjs.map +4 -4
  33. package/dist/plugin/index.d.ts.map +1 -1
  34. package/dist/plugin/index.js +803 -270
  35. package/dist/plugin/index.js.map +4 -4
  36. package/dist/spec/index.cjs.map +1 -1
  37. package/dist/spec/index.js.map +1 -1
  38. package/dist/types.d.ts +18 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/version.d.ts +17 -0
  41. package/dist/version.d.ts.map +1 -0
  42. package/package.json +2 -1
@@ -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.33.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 }";
@@ -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}) {`,
@@ -6078,6 +6555,24 @@ function emit(p, ctx, pos) {
6078
6555
  return instrument(dispatch());
6079
6556
  }
6080
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
+ }
6081
6576
  const def = p._def;
6082
6577
  switch (def.tag) {
6083
6578
  case "literal":
@@ -6499,9 +6994,36 @@ function ruleDependencies(ruleMap) {
6499
6994
  }
6500
6995
  return deps;
6501
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
+ }
6502
7022
  function compile(combinator, mapFnSources, opts) {
7023
+ const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
6503
7024
  markUnusedValues(combinator);
6504
7025
  const grammarTrivia = combinator._meta.grammarTrivia;
7026
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
6505
7027
  const ctx = {
6506
7028
  vars: 0,
6507
7029
  indent: 1,
@@ -6523,7 +7045,8 @@ function compile(combinator, mapFnSources, opts) {
6523
7045
  recovery: opts?.recovery ?? false,
6524
7046
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
6525
7047
  lazyUsage: analyzeLazyUsage(combinator),
6526
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7048
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7049
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6527
7050
  };
6528
7051
  const r = emit(combinator, ctx, "_pos");
6529
7052
  const coverageRootRuleId = ctx.coverage?.plan.rules.get(combinator);
@@ -6587,6 +7110,7 @@ function compile(combinator, mapFnSources, opts) {
6587
7110
  source,
6588
7111
  inlineExpression,
6589
7112
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
7113
+ ...gatingReport === void 0 ? {} : { gating: gatingReport },
6590
7114
  parse(input, pos = 0) {
6591
7115
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
6592
7116
  },
@@ -6622,6 +7146,7 @@ function compileRuleMap(ruleMap, opts) {
6622
7146
  for (const [, rule] of ruleMap) markUnusedValues(rule);
6623
7147
  const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
6624
7148
  const grammarTrivia = opts?.trivia;
7149
+ const grammarScanSkip = opts?.scanSkip ?? ruleMap.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6625
7150
  const ctx = {
6626
7151
  vars: 0,
6627
7152
  indent: 1,
@@ -6643,7 +7168,8 @@ function compileRuleMap(ruleMap, opts) {
6643
7168
  recovery: opts?.recovery ?? false,
6644
7169
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(ruleMap.map(([, rule]) => rule), coverageWinners) } } : {},
6645
7170
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6646
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7171
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7172
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6647
7173
  };
6648
7174
  const seenNames = /* @__PURE__ */ new Map();
6649
7175
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6657,9 +7183,14 @@ function compileRuleMap(ruleMap, opts) {
6657
7183
  ctx.ruleNames = ruleNames;
6658
7184
  const perEntry = ruleMap.map(([key, rule]) => {
6659
7185
  const savedTrivia = ctx.activeTrivia;
6660
- 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
+ }
6661
7191
  const r = emit(rule, ctx, "_pos");
6662
7192
  ctx.activeTrivia = savedTrivia;
7193
+ ctx.activeScanSkip = savedScanSkip;
6663
7194
  return { key, rule, r };
6664
7195
  });
6665
7196
  const derivedSrcs = ctx.mapFnSrcs.length === ctx.mapFns.length && ctx.mapFnSrcs.every((s) => s !== null) ? ctx.mapFnSrcs : void 0;
@@ -6732,6 +7263,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6732
7263
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
6733
7264
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
6734
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);
6735
7267
  const ruleMap = ruleMapArg.filter(([, val]) => {
6736
7268
  const d = val._def;
6737
7269
  if (d.tag !== "lazy") return true;
@@ -6769,7 +7301,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
6769
7301
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6770
7302
  ns,
6771
7303
  deferFirstSetRefs: true,
6772
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7304
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7305
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6773
7306
  };
6774
7307
  const seen = /* @__PURE__ */ new Map();
6775
7308
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6833,9 +7366,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
6833
7366
  ctx.failLabel = "_pfail";
6834
7367
  ctx.recordFail = true;
6835
7368
  const savedTrivia = ctx.activeTrivia;
6836
- 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
+ }
6837
7374
  const body = emit(resolved, ctx, "_pos");
6838
7375
  ctx.activeTrivia = savedTrivia;
7376
+ ctx.activeScanSkip = savedScanSkip;
6839
7377
  ctx.indent = savedIndent;
6840
7378
  ctx.failLabel = savedFail;
6841
7379
  ctx.recordFail = savedRec;
@@ -6882,6 +7420,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6882
7420
  }
6883
7421
  const firstSets = /* @__PURE__ */ new Map();
6884
7422
  const firstSetRecipes = /* @__PURE__ */ new Map();
7423
+ const nullable2 = /* @__PURE__ */ new Map();
6885
7424
  for (const [key, rule] of ruleMap) {
6886
7425
  let resolved = rule;
6887
7426
  const d = rule._def;
@@ -6893,15 +7432,18 @@ function compileLinkable(ruleMapArg, ns, opts) {
6893
7432
  }
6894
7433
  }
6895
7434
  firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
6896
- 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));
6897
7437
  }
6898
7438
  return {
7439
+ v: PARSEMAN_VERSION,
6899
7440
  ns,
6900
7441
  keys: perEntry.map((e) => e.key),
6901
7442
  prelude,
6902
7443
  ruleFns,
6903
7444
  wrappers,
6904
7445
  firstSets,
7446
+ nullable: nullable2,
6905
7447
  firstSetRecipes,
6906
7448
  deps: ruleDependencies(ruleMap),
6907
7449
  needsEmptyTl: !!ctx.needsEmptyTl,
@@ -6980,101 +7522,6 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
6980
7522
  ].join("\n");
6981
7523
  }
6982
7524
 
6983
- // src/combinators/scanTo.ts
6984
- function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
6985
- const meta = {
6986
- firstSet: any(),
6987
- canMatchNewline: true,
6988
- isTrivia: false
6989
- };
6990
- return {
6991
- _tag: "scanTo",
6992
- _meta: meta,
6993
- _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
6994
- parse(input, pos, ctx) {
6995
- let cur = pos;
6996
- const probeCtx = {
6997
- trackLines: ctx.trackLines,
6998
- state: ctx.state,
6999
- ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
7000
- };
7001
- const emit2 = (end) => {
7002
- if (end > pos && cstCaptureActive(ctx)) {
7003
- const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
7004
- pushCstLeaf(ctx, leaf2);
7005
- }
7006
- };
7007
- while (cur < input.length) {
7008
- const s = sentinel.parse(input, cur, probeCtx);
7009
- if (s.ok) {
7010
- emit2(cur);
7011
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
7012
- }
7013
- let advanced = false;
7014
- for (const skipper of skip2) {
7015
- const r = skipper.parse(input, cur, probeCtx);
7016
- if (r.ok && r.span.end > cur) {
7017
- cur = r.span.end;
7018
- advanced = true;
7019
- break;
7020
- }
7021
- }
7022
- if (!advanced) cur++;
7023
- }
7024
- if (orEOF) {
7025
- emit2(cur);
7026
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
7027
- }
7028
- const sentDef = sentinel._def;
7029
- const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
7030
- return { ok: false, expected, span: { start: pos, end: cur } };
7031
- }
7032
- };
7033
- }
7034
- function balanced(open, close, options = {}) {
7035
- const self = ref();
7036
- const skips = options.skip ?? [];
7037
- const stop = /* @__PURE__ */ new Set([open, close]);
7038
- let bounded = true;
7039
- for (const sk of skips) {
7040
- const cs = firstSetClassChars(sk);
7041
- if (cs === null) {
7042
- bounded = false;
7043
- break;
7044
- }
7045
- for (const ch of cs) stop.add(ch);
7046
- }
7047
- const cls = [...stop].map(escapeClassChar).join("");
7048
- const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
7049
- const inner = many(choice(self, ...skips, content));
7050
- const combi = transform(
7051
- sequence(literal(open), inner, expect(literal(close))),
7052
- // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
7053
- // ParseError (recovered close). `c` is the close string or a ParseError.
7054
- ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
7055
- );
7056
- if (combi._def.tag === "transform") {
7057
- combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
7058
- combi._def.recognitionOnly = true;
7059
- }
7060
- self.define(combi);
7061
- return combi;
7062
- }
7063
- function firstSetClassChars(c) {
7064
- const fs2 = c._meta.firstSet;
7065
- if (fs2.kind === "empty") return [];
7066
- if (fs2.kind !== "ranges") return null;
7067
- const out = [];
7068
- for (const { lo, hi } of fs2.ranges) {
7069
- if (hi - lo > 8) return null;
7070
- for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
7071
- }
7072
- return out;
7073
- }
7074
- function escapeClassChar(ch) {
7075
- return ch.replace(/[\\\]^-]/g, "\\$&");
7076
- }
7077
-
7078
7525
  // src/combinators/withCtx.ts
7079
7526
  function withCtx(extra, combinator) {
7080
7527
  const meta = {
@@ -7131,7 +7578,7 @@ function childrenOf2(def) {
7131
7578
  return [];
7132
7579
  }
7133
7580
  }
7134
- var ruleNameOf = (c) => c._ruleName;
7581
+ var ruleNameOf2 = (c) => c._ruleName;
7135
7582
  function lazyTarget(c) {
7136
7583
  if (c._def.tag !== "lazy") return null;
7137
7584
  try {
@@ -7142,9 +7589,9 @@ function lazyTarget(c) {
7142
7589
  }
7143
7590
  var Unserializable = class extends Error {
7144
7591
  };
7145
- function serializeRuleMap(ruleMap) {
7592
+ function serializeRuleMap(ruleMap, scanSkip) {
7146
7593
  try {
7147
- return new Serializer(ruleMap).run();
7594
+ return new Serializer(ruleMap, scanSkip ?? []).run();
7148
7595
  } catch (e) {
7149
7596
  if (e instanceof Unserializable) {
7150
7597
  if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${e.message}`);
@@ -7198,15 +7645,15 @@ function evalRuleMapIR(ir) {
7198
7645
  if (Array.isArray(it)) {
7199
7646
  const [src, comb] = it;
7200
7647
  gateSrcs.push(src);
7201
- let gate;
7648
+ let gate2;
7202
7649
  try {
7203
- gate = (0, eval)(`(${src})`);
7650
+ gate2 = (0, eval)(`(${src})`);
7204
7651
  } catch {
7205
- gate = () => {
7652
+ gate2 = () => {
7206
7653
  throw new Error("IR gate fn not materialized");
7207
7654
  };
7208
7655
  }
7209
- return { gate, combinator: comb };
7656
+ return { gate: gate2, combinator: comb };
7210
7657
  }
7211
7658
  gateSrcs.push(null);
7212
7659
  return it;
@@ -7305,40 +7752,75 @@ var Serializer = class {
7305
7752
  decls = [];
7306
7753
  ruleValues = /* @__PURE__ */ new Set();
7307
7754
  ruleMap;
7308
- constructor(ruleMap) {
7755
+ scanSkip;
7756
+ constructor(ruleMap, scanSkip = []) {
7309
7757
  this.ruleMap = ruleMap;
7758
+ this.scanSkip = scanSkip;
7310
7759
  for (const [, c] of ruleMap) this.ruleValues.add(this.body(c));
7311
7760
  }
7761
+ /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
7762
+ * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
7763
+ * serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
7764
+ * outside the `(g) => …` factory where `g` is bound. A unit that does reference a
7765
+ * rule can't be carried in options; bail to the full-pieces fallback. */
7766
+ assertNoRuleRef(c, seen) {
7767
+ if (seen.has(c)) return;
7768
+ seen.add(c);
7769
+ if (c._def.tag === "lazy") {
7770
+ if (ruleNameOf2(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
7771
+ const target = lazyTarget(c);
7772
+ if (target) this.assertNoRuleRef(target, seen);
7773
+ return;
7774
+ }
7775
+ for (const child of childrenOf2(c._def)) this.assertNoRuleRef(child, seen);
7776
+ }
7312
7777
  /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
7313
7778
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7314
7779
  * `g[name]` ref — only the top-level entry is unwrapped. */
7315
7780
  body(c) {
7316
- return c._def.tag === "lazy" && ruleNameOf(c) !== void 0 ? lazyTarget(c) ?? c : c;
7781
+ return c._def.tag === "lazy" && ruleNameOf2(c) !== void 0 ? lazyTarget(c) ?? c : c;
7317
7782
  }
7318
7783
  run() {
7319
7784
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
7785
+ for (const c of this.scanSkip) {
7786
+ this.assertNoRuleRef(c, /* @__PURE__ */ new Set());
7787
+ this.analyze(c, /* @__PURE__ */ new Set());
7788
+ }
7320
7789
  let n = 0;
7321
7790
  for (const [c, count] of this.counts) {
7322
7791
  if (this.ruleValues.has(c)) continue;
7323
7792
  if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`);
7324
7793
  }
7794
+ const outerDecls = [];
7795
+ const innerDecls = [];
7796
+ let optionsArg = "";
7797
+ if (this.scanSkip.length > 0) {
7798
+ this.decls = outerDecls;
7799
+ const units = this.scanSkip.map((c) => this.ref(c, null));
7800
+ optionsArg = `{ scanSkip: [${units.join(", ")}] }, `;
7801
+ }
7802
+ this.decls = innerDecls;
7325
7803
  const body = this.ruleMap.map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c))}`).join(",\n");
7326
- if (this.decls.length === 0) return `rules((g) => ({
7804
+ const factory = innerDecls.length === 0 ? `(g) => ({
7327
7805
  ${body}
7328
- }))`;
7329
- return `rules((g) => {
7330
- ${this.decls.map((d) => " " + d).join("\n")}
7806
+ })` : `(g) => {
7807
+ ${innerDecls.map((d) => " " + d).join("\n")}
7331
7808
  return ({
7332
7809
  ${body}
7333
7810
  })
7334
- })`;
7811
+ }`;
7812
+ const call = `rules(${optionsArg}${factory})`;
7813
+ return outerDecls.length === 0 ? call : `(() => {
7814
+ ${outerDecls.map((d) => " " + d).join("\n")}
7815
+ return ${call}
7816
+ })()`;
7335
7817
  }
7336
7818
  /** Count identity references and flag self-referential subtrees. `active` is the
7337
7819
  * set of combinators on the current DFS path (to detect a lazy pointing back). */
7338
7820
  analyze(c, active) {
7339
7821
  const def = c._def;
7340
7822
  if (def.tag === "lazy") {
7341
- if (ruleNameOf(c) !== void 0) return;
7823
+ if (ruleNameOf2(c) !== void 0) return;
7342
7824
  const target = lazyTarget(c);
7343
7825
  if (!target) throw new Unserializable("unresolved unnamed ref");
7344
7826
  if (active.has(target)) {
@@ -7390,7 +7872,7 @@ ${body}
7390
7872
  const kid = (x) => this.ref(x, selfOf);
7391
7873
  switch (def.tag) {
7392
7874
  case "lazy": {
7393
- if (ruleNameOf(c) !== void 0) return `g[${JSON.stringify(ruleNameOf(c))}]`;
7875
+ if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
7394
7876
  const target = lazyTarget(c);
7395
7877
  if (selfOf && target === selfOf.comb) return selfOf.var;
7396
7878
  return this.ref(target, selfOf);
@@ -7444,7 +7926,7 @@ ${body}
7444
7926
  case "skip":
7445
7927
  return `skip(${kid(def.main)}, ${kid(def.skipped)})`;
7446
7928
  case "scanTo":
7447
- return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}], orEOF: ${def.orEOF} })`;
7929
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}]${def.raw ? ", raw: true" : ""}, orEOF: ${def.orEOF} })`;
7448
7930
  case "transform": {
7449
7931
  if (def.fnSrc === void 0) throw new Unserializable("transform without fnSrc");
7450
7932
  return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ", true" : ""})`;
@@ -7525,6 +8007,18 @@ function pickPieces(pieces, names) {
7525
8007
  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);
7526
8008
  }
7527
8009
  function fusedBody(pieces) {
8010
+ for (const p of pieces) {
8011
+ if (p.v === void 0) {
8012
+ throw new Error(
8013
+ `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.`
8014
+ );
8015
+ }
8016
+ if (p.v !== PARSEMAN_VERSION) {
8017
+ throw new Error(
8018
+ `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.`
8019
+ );
8020
+ }
8021
+ }
7528
8022
  const winner = /* @__PURE__ */ new Map();
7529
8023
  for (const p of pieces) for (const k of p.keys) winner.set(k, p);
7530
8024
  for (const [k, p] of winner) {
@@ -7550,23 +8044,38 @@ function fusedBody(pieces) {
7550
8044
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
7551
8045
  const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
7552
8046
  const ANY2 = { kind: "any" };
8047
+ const EMPTY3 = { kind: "empty" };
7553
8048
  const recipes = /* @__PURE__ */ new Map();
7554
8049
  const shallow = /* @__PURE__ */ new Map();
8050
+ const nullable2 = /* @__PURE__ */ new Map();
7555
8051
  for (const [k, p] of winner) {
7556
8052
  const r = p.firstSetRecipes?.get(k);
7557
8053
  if (r) recipes.set(k, r);
7558
8054
  const fs2 = p.firstSets?.get(k);
7559
8055
  if (fs2) shallow.set(k, fs2);
8056
+ const nu = p.nullable?.get(k);
8057
+ if (nu !== void 0) nullable2.set(k, nu);
7560
8058
  }
8059
+ const knownRef = (ref2) => recipes.has(ref2) || shallow.has(ref2);
8060
+ const refFS = (ref2) => knownRef(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2;
8061
+ const refNullable = (ref2) => knownRef(ref2) ? nullable2.get(ref2) ?? true : true;
8062
+ const resolveRecipe = (r) => {
8063
+ let out = EMPTY3;
8064
+ for (const chain of r.alts) {
8065
+ for (const seg of chain) {
8066
+ out = union(out, seg.ref !== void 0 ? refFS(seg.ref) : seg.set);
8067
+ const skippable = seg.ref !== void 0 ? seg.nullable || refNullable(seg.ref) : seg.nullable;
8068
+ if (!skippable) break;
8069
+ }
8070
+ }
8071
+ return out;
8072
+ };
7561
8073
  const finalFS = /* @__PURE__ */ new Map();
7562
- for (const [k] of winner) finalFS.set(k, recipes.get(k)?.concrete ?? shallow.get(k) ?? ANY2);
8074
+ for (const [k] of winner) finalFS.set(k, recipes.has(k) ? EMPTY3 : shallow.get(k) ?? ANY2);
7563
8075
  for (let iter = 0; iter <= recipes.size; iter++) {
7564
8076
  let changed = false;
7565
8077
  for (const [k, r] of recipes) {
7566
- let fs2 = r.concrete;
7567
- for (const ref2 of r.refs) {
7568
- fs2 = union(fs2, recipes.has(ref2) || shallow.has(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2);
7569
- }
8078
+ const fs2 = resolveRecipe(r);
7570
8079
  if (JSON.stringify(finalFS.get(k)) !== JSON.stringify(fs2)) {
7571
8080
  finalFS.set(k, fs2);
7572
8081
  changed = true;
@@ -7614,8 +8123,8 @@ function materializePiece(p, trivia2, captureTerminals = false) {
7614
8123
  return pieces;
7615
8124
  }
7616
8125
 
7617
- // src/combinators/guard.ts
7618
- function guard(predicate) {
8126
+ // src/combinators/gate.ts
8127
+ function gate(predicate) {
7619
8128
  const meta = {
7620
8129
  firstSet: { kind: "any" },
7621
8130
  canMatchNewline: false,
@@ -7872,17 +8381,17 @@ function isGatedArmExpr(e) {
7872
8381
  return hasGate && hasCombinator;
7873
8382
  }
7874
8383
  function gatedArmParts(e) {
7875
- let gate;
8384
+ let gate2;
7876
8385
  let combinator;
7877
8386
  for (const prop of e.properties) {
7878
8387
  if (prop.type !== "Property") return null;
7879
8388
  const op = prop;
7880
8389
  const name = propName(op);
7881
- if (name === "gate") gate = op.value;
8390
+ if (name === "gate") gate2 = op.value;
7882
8391
  else if (name === "combinator") combinator = op.value;
7883
8392
  else return null;
7884
8393
  }
7885
- return gate && combinator ? { gate, combinator } : null;
8394
+ return gate2 && combinator ? { gate: gate2, combinator } : null;
7886
8395
  }
7887
8396
  function isCombinator(v2) {
7888
8397
  return !!v2 && typeof v2 === "object" && "_def" in v2;
@@ -8084,14 +8593,14 @@ function exprToCombi(node2, scope, code, mfs) {
8084
8593
  return null;
8085
8594
  }
8086
8595
  }
8087
- if (callee.name === "guard") {
8596
+ if (callee.name === "gate" || callee.name === "guard") {
8088
8597
  if (code === void 0 || mfs === void 0) return null;
8089
8598
  const [predArg] = node2.arguments;
8090
8599
  if (!predArg || predArg.type === "SpreadElement") return null;
8091
8600
  const predSrc = stripTsFromSource(predArg, code);
8092
8601
  mfs.push(predSrc);
8093
8602
  try {
8094
- const combi = guard(() => true);
8603
+ const combi = gate(() => true);
8095
8604
  if (combi._def.tag !== "guard") return null;
8096
8605
  combi._def.predSrc = predSrc;
8097
8606
  return combi;
@@ -8715,21 +9224,32 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8715
9224
  return null;
8716
9225
  }
8717
9226
  const optionsArg = optionsFirst ? arg0 : arg1;
8718
- const triviaValue = optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
8719
- (p) => p.key?.name === "trivia"
9227
+ const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
9228
+ (p) => p.key?.name === name
8720
9229
  )?.value : void 0;
9230
+ const triviaValue = optionValue("trivia");
8721
9231
  const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
8722
- return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {} };
9232
+ const scanSkipValue = optionValue("scanSkip");
9233
+ const gScanSkip = scanSkipValue ? evaluateCombinatorArray(scanSkipValue, scope, code) ?? void 0 : void 0;
9234
+ if (gScanSkip) {
9235
+ for (const rule of ruleMap.values()) {
9236
+ if (rule && !rule._meta.isTrivia) {
9237
+ ;
9238
+ rule._meta.grammarScanSkip = gScanSkip;
9239
+ }
9240
+ }
9241
+ }
9242
+ return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
8723
9243
  };
8724
9244
  const compileRulesFactory = (init, label2) => {
8725
9245
  const evaluated = evaluateRulesFactory(init, label2);
8726
9246
  if (!evaluated) return null;
8727
- const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, recovery, coverage: grammarCoverage });
9247
+ const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
8728
9248
  if (!compiled) {
8729
9249
  warn(init.start, `${label2}: rule map couldn't be inlined`);
8730
9250
  return null;
8731
9251
  }
8732
- return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {} };
9252
+ return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
8733
9253
  };
8734
9254
  const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
8735
9255
  const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
@@ -8737,13 +9257,14 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8737
9257
  const isPickCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "pick";
8738
9258
  const localRuleMaps = /* @__PURE__ */ new Map();
8739
9259
  const localGrammarTrivia = /* @__PURE__ */ new Map();
9260
+ const localGrammarScanSkip = /* @__PURE__ */ new Map();
8740
9261
  const nsFor = (label2) => `_${(0, import_node_crypto.createHash)("sha1").update(`${id}#${label2}`).digest("hex").slice(0, 8)}_`;
8741
9262
  const stripIndent = (s) => s.replace(/\n[ \t]+/g, "\n");
8742
9263
  const serializePieces = (p) => {
8743
9264
  const mapLit = (m, stripVals = false) => `new Map([${[...m].map(
8744
9265
  ([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(stripVals && typeof v2 === "string" ? stripIndent(v2) : v2)}]`
8745
9266
  ).join(", ")}])`;
8746
- 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: [] }`;
9267
+ 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: [] }`;
8747
9268
  };
8748
9269
  const isSpread = (it) => "__spreadLocal" in it;
8749
9270
  const isIR = (it) => "ir" in it && !("ruleFns" in it);
@@ -8854,12 +9375,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8854
9375
  }
8855
9376
  return pieces;
8856
9377
  };
8857
- const localCarried = (rm, label2, composing) => {
9378
+ const localCarried = (rm, label2, composing, scanSkip) => {
8858
9379
  const entries = [...rm];
8859
9380
  const ns = nsFor(label2);
8860
- const ir = serializeRuleMap(entries);
9381
+ const ir = serializeRuleMap(entries, scanSkip);
8861
9382
  if (ir) return { carried: [{ ns, ir }] };
8862
- const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, recovery });
9383
+ const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
8863
9384
  return p ? { carried: [p] } : null;
8864
9385
  };
8865
9386
  const argPieces = (arg, label2, composing) => {
@@ -8884,18 +9405,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8884
9405
  }
8885
9406
  }
8886
9407
  if (isRulesCall(arg)) {
8887
- const rulesArgs = arg.arguments;
8888
- const a0 = rulesArgs[0];
8889
- const a1 = rulesArgs[1];
8890
- const optionsFirst = a0?.type === "ObjectExpression";
8891
- const factory = optionsFirst ? a1 : a0;
8892
- const rm = factory ? evaluateParserFactory(factory, scope, code, []) : null;
8893
- return rm ? localCarried(rm, label2, composing) : null;
9408
+ const evaluated = evaluateRulesFactory(arg, label2);
9409
+ return evaluated ? localCarried(evaluated.ruleMap, label2, composing, evaluated.scanSkip) : null;
8894
9410
  }
8895
9411
  if (arg.type === "Identifier") {
8896
9412
  const name = arg.name;
8897
9413
  const rm = localRuleMaps.get(name);
8898
- if (rm) return localCarried(rm, label2, composing);
9414
+ if (rm) return localCarried(rm, label2, composing, localGrammarScanSkip.get(name));
8899
9415
  const composed = localComposedCarried.get(name);
8900
9416
  if (composed) return { carried: composed };
8901
9417
  const pieces = importedPieces(name);
@@ -8983,11 +9499,15 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
8983
9499
  }
8984
9500
  const localArg = elements[elements.length - 1];
8985
9501
  let localRules = null;
9502
+ let localScanSkip;
8986
9503
  if (isRulesCall(localArg)) {
8987
9504
  const evaluated = evaluateRulesFactory(localArg, `composeLeaf${init.start}`);
8988
9505
  localRules = evaluated?.ruleMap ?? null;
9506
+ localScanSkip = evaluated?.scanSkip;
8989
9507
  } else if (localArg.type === "Identifier") {
8990
- localRules = localRuleMaps.get(localArg.name) ?? null;
9508
+ const name = localArg.name;
9509
+ localRules = localRuleMaps.get(name) ?? null;
9510
+ localScanSkip = localGrammarScanSkip.get(name);
8991
9511
  }
8992
9512
  if (!localRules) {
8993
9513
  warn(init.start, "composeLeaf(): final argument must be a local rules() map");
@@ -9005,7 +9525,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9005
9525
  }
9006
9526
  try {
9007
9527
  const localNs = nsFor(`composeLeaf${init.start}`);
9008
- const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, recovery });
9528
+ const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
9009
9529
  if (!plainLocalPiece) {
9010
9530
  warn(init.start, "composeLeaf(): local rules could not be statically compiled");
9011
9531
  return null;
@@ -9107,15 +9627,19 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9107
9627
  if (!compiledRules) continue;
9108
9628
  localRuleMaps.set(varName, compiledRules.ruleMap);
9109
9629
  if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia);
9630
+ if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip);
9110
9631
  let replacement = withCoverageDefinitions(
9111
9632
  compiledRules.replacement,
9112
9633
  compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(compiledRules.replacement)
9113
9634
  );
9114
9635
  if (exportPrefix) {
9115
9636
  const ns = nsFor(varName);
9116
- const pieces = compileLinkable([...compiledRules.ruleMap], ns, { recovery });
9637
+ const pieces = compileLinkable([...compiledRules.ruleMap], ns, {
9638
+ ...compiledRules.scanSkip ? { scanSkip: compiledRules.scanSkip } : {},
9639
+ recovery
9640
+ });
9117
9641
  if (pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
9118
- const ir = serializeRuleMap([...compiledRules.ruleMap]);
9642
+ const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
9119
9643
  replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
9120
9644
  }
9121
9645
  }
@@ -9235,9 +9759,18 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9235
9759
  ms.overwrite(imp.start, imp.end, stripped);
9236
9760
  }
9237
9761
  }
9238
- for (const { start, end, replacement } of (runtimeComposeFallback ? [] : replacements).sort((a, b) => b.start - a.start)) {
9762
+ const applied = runtimeComposeFallback ? [] : replacements;
9763
+ for (const { start, end, replacement } of applied.slice().sort((a, b) => b.start - a.start)) {
9239
9764
  ms.overwrite(start, end, replacement);
9240
9765
  }
9766
+ if (applied.length > 0) {
9767
+ ms.prepend(
9768
+ `// Generated by parseman v${PARSEMAN_VERSION} \u2014 DO NOT EDIT.
9769
+ // Version-locked: compile AND fuse/link this artifact with parseman v${PARSEMAN_VERSION} ONLY.
9770
+ // Parseman does not read artifacts across versions; recompile if the version differs.
9771
+ `
9772
+ );
9773
+ }
9241
9774
  const unlowered = endLoweringCapture();
9242
9775
  if (warnUnloweredRegex) {
9243
9776
  for (const src of unlowered) {