parseman 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/analysis/gating.d.ts +115 -0
  2. package/dist/analysis/gating.d.ts.map +1 -0
  3. package/dist/combinators/first-set.d.ts +25 -0
  4. package/dist/combinators/first-set.d.ts.map +1 -1
  5. package/dist/combinators/gate.d.ts +24 -0
  6. package/dist/combinators/gate.d.ts.map +1 -0
  7. package/dist/combinators/grammar.d.ts.map +1 -1
  8. package/dist/combinators/guard.d.ts +5 -12
  9. package/dist/combinators/guard.d.ts.map +1 -1
  10. package/dist/combinators/parser.d.ts +7 -0
  11. package/dist/combinators/parser.d.ts.map +1 -1
  12. package/dist/combinators/regex.d.ts.map +1 -1
  13. package/dist/combinators/scanTo.d.ts +45 -3
  14. package/dist/combinators/scanTo.d.ts.map +1 -1
  15. package/dist/compiler/codegen.d.ts +78 -14
  16. package/dist/compiler/codegen.d.ts.map +1 -1
  17. package/dist/compiler/linker.d.ts.map +1 -1
  18. package/dist/functional/run.d.ts.map +1 -1
  19. package/dist/index.cjs +715 -237
  20. package/dist/index.cjs.map +4 -4
  21. package/dist/index.d.ts +5 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +715 -237
  24. package/dist/index.js.map +4 -4
  25. package/dist/language-service/index.cjs +2 -0
  26. package/dist/language-service/index.cjs.map +2 -2
  27. package/dist/language-service/index.js +2 -0
  28. package/dist/language-service/index.js.map +2 -2
  29. package/dist/plugin/index.cjs +741 -249
  30. package/dist/plugin/index.cjs.map +4 -4
  31. package/dist/plugin/index.d.ts.map +1 -1
  32. package/dist/plugin/index.js +741 -249
  33. package/dist/plugin/index.js.map +4 -4
  34. package/dist/spec/index.cjs.map +1 -1
  35. package/dist/spec/index.js.map +1 -1
  36. package/dist/types.d.ts +18 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/version.d.ts +17 -0
  39. package/dist/version.d.ts.map +1 -0
  40. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -87,10 +87,13 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
87
87
  return true;
88
88
  }
89
89
  }
90
+ function isZeroWidthAssertion(p) {
91
+ return p._def.tag === "not";
92
+ }
90
93
  function sequenceFirstSet(parsers) {
91
94
  let fs = empty();
92
95
  for (const p of parsers) {
93
- fs = union(fs, p._meta.firstSet);
96
+ if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet);
94
97
  if (!matchesEmpty(p)) return fs;
95
98
  }
96
99
  return fs;
@@ -120,7 +123,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
120
123
  case "sequence": {
121
124
  let out = empty();
122
125
  for (const term of d.parsers) {
123
- out = union(out, fs(term));
126
+ if (!isZeroWidthAssertion(term)) out = union(out, fs(term));
124
127
  if (!matchesEmpty(term)) return out;
125
128
  }
126
129
  return out;
@@ -408,9 +411,9 @@ var SPACE_RANGES = [
408
411
  [12288, 12288],
409
412
  [65279, 65279]
410
413
  ];
411
- function shorthandRanges(ch) {
412
- if (ch === "d") return [[48, 57]];
413
- if (ch === "s") return SPACE_RANGES;
414
+ function shorthandRanges(ch2) {
415
+ if (ch2 === "d") return [[48, 57]];
416
+ if (ch2 === "s") return SPACE_RANGES;
414
417
  return [[48, 57], [65, 90], [97, 122], [95, 95]];
415
418
  }
416
419
  function readUnicodeEscape(body, i) {
@@ -423,9 +426,9 @@ function parseClassRanges(body) {
423
426
  const ranges = [];
424
427
  let i = 0;
425
428
  const readAtom = () => {
426
- const ch = body[i];
427
- if (ch === void 0) return null;
428
- if (ch === "\\") {
429
+ const ch2 = body[i];
430
+ if (ch2 === void 0) return null;
431
+ if (ch2 === "\\") {
429
432
  const uni = readUnicodeEscape(body, i);
430
433
  if (uni) {
431
434
  i = uni.next;
@@ -439,8 +442,8 @@ function parseClassRanges(body) {
439
442
  if (e >= "a" && e <= "z" || e >= "A" && e <= "Z") return null;
440
443
  return { cp: e.codePointAt(0) };
441
444
  }
442
- i += ch.length;
443
- return { cp: ch.codePointAt(0) };
445
+ i += ch2.length;
446
+ return { cp: ch2.codePointAt(0) };
444
447
  };
445
448
  while (i < body.length) {
446
449
  const lo = readAtom();
@@ -507,12 +510,35 @@ function shortScanner(source, flags) {
507
510
  return minOne && end === pos ? null : end;
508
511
  };
509
512
  }
513
+ function asciiCaseFold(fs) {
514
+ if (fs.kind !== "ranges") return fs;
515
+ const ranges = fs.ranges.map((r) => ({ lo: r.lo, hi: r.hi }));
516
+ const add = (lo, hi) => {
517
+ if (lo <= hi) ranges.push({ lo, hi });
518
+ };
519
+ for (const r of fs.ranges) {
520
+ const uLo = Math.max(r.lo, 65), uHi = Math.min(r.hi, 90);
521
+ add(uLo + 32, uHi + 32);
522
+ const lLo = Math.max(r.lo, 97), lHi = Math.min(r.hi, 122);
523
+ add(lLo - 32, lHi - 32);
524
+ }
525
+ ranges.sort((a, b) => a.lo - b.lo);
526
+ const merged = [];
527
+ for (const r of ranges) {
528
+ const last = merged[merged.length - 1];
529
+ if (last && r.lo <= last.hi + 1) last.hi = Math.max(last.hi, r.hi);
530
+ else merged.push({ lo: r.lo, hi: r.hi });
531
+ }
532
+ return { kind: "ranges", ranges: merged };
533
+ }
510
534
  function regex(pattern, flags = "") {
511
535
  const source = typeof pattern === "string" ? pattern : pattern.source;
512
536
  const resolvedFlags = typeof pattern === "string" ? flags : pattern.flags;
513
537
  const anchored = new RegExp(source, "y" + resolvedFlags.replace(/[gy]/g, ""));
514
538
  const scan = shortScanner(source, resolvedFlags);
515
- const { firstSet: firstSet2, canMatchNewline } = (firstSetAnalyzer ?? permissiveFirstSet)(source);
539
+ const raw = (firstSetAnalyzer ?? permissiveFirstSet)(source);
540
+ const firstSet2 = !resolvedFlags.includes("i") ? raw.firstSet : resolvedFlags.includes("u") || resolvedFlags.includes("v") ? any() : asciiCaseFold(raw.firstSet);
541
+ const canMatchNewline = raw.canMatchNewline;
516
542
  const meta = { firstSet: firstSet2, canMatchNewline, isTrivia: false };
517
543
  return {
518
544
  _tag: "regex",
@@ -567,8 +593,8 @@ function parseRegex(src) {
567
593
  const parseSeq = () => {
568
594
  const parts = [];
569
595
  while (i < src.length) {
570
- const ch = src[i];
571
- if (ch === "|" || ch === ")") break;
596
+ const ch2 = src[i];
597
+ if (ch2 === "|" || ch2 === ")") break;
572
598
  parts.push(parseQuantified());
573
599
  }
574
600
  if (parts.length === 0) return EMPTY2;
@@ -576,18 +602,18 @@ function parseRegex(src) {
576
602
  };
577
603
  const parseQuantified = () => {
578
604
  const atom = parseAtom();
579
- const ch = src[i];
605
+ const ch2 = src[i];
580
606
  let min = null;
581
- if (ch === "*") {
607
+ if (ch2 === "*") {
582
608
  min = 0;
583
609
  i++;
584
- } else if (ch === "+") {
610
+ } else if (ch2 === "+") {
585
611
  min = 1;
586
612
  i++;
587
- } else if (ch === "?") {
613
+ } else if (ch2 === "?") {
588
614
  min = 0;
589
615
  i++;
590
- } else if (ch === "{") {
616
+ } else if (ch2 === "{") {
591
617
  min = parseBraceQuantifier();
592
618
  }
593
619
  if (min === null) return atom;
@@ -603,8 +629,8 @@ function parseRegex(src) {
603
629
  return Number.parseInt(m[1], 10);
604
630
  };
605
631
  const parseAtom = () => {
606
- const ch = src[i];
607
- switch (ch) {
632
+ const ch2 = src[i];
633
+ switch (ch2) {
608
634
  case "(":
609
635
  return parseGroup();
610
636
  case "[":
@@ -625,8 +651,8 @@ function parseRegex(src) {
625
651
  return charNode(fromRange(123, 123), false);
626
652
  // a `{` with nothing to quantify is a literal brace
627
653
  default:
628
- i += ch.length;
629
- return charNode(fromRange(ch.codePointAt(0), ch.codePointAt(0)), ch === "\n");
654
+ i += ch2.length;
655
+ return charNode(fromRange(ch2.codePointAt(0), ch2.codePointAt(0)), ch2 === "\n");
630
656
  }
631
657
  };
632
658
  const parseGroup = () => {
@@ -1066,8 +1092,8 @@ function choice(...args) {
1066
1092
  }
1067
1093
  }
1068
1094
  if (idx >= 0) {
1069
- const gate = gates[idx];
1070
- if (gate && !gate(ctx.state)) {
1095
+ const gate2 = gates[idx];
1096
+ if (gate2 && !gate2(ctx.state)) {
1071
1097
  return { ok: false, expected: deriveExpected(parsers[idx]), span: { start: pos, end: pos } };
1072
1098
  }
1073
1099
  const result = parsers[idx].parse(input, pos, ctx);
@@ -1413,11 +1439,11 @@ function tryFastLabeledScan(input, cur, trivia2) {
1413
1439
  function recordTriviaChunks(ctx, chunks) {
1414
1440
  const kinds = ctx.triviaKindLabels;
1415
1441
  const mask = ctx._triviaCaptureMask;
1416
- for (const ch of chunks) {
1417
- pushTriviaLogEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1442
+ for (const ch2 of chunks) {
1443
+ pushTriviaLogEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1418
1444
  if (ctx.captureTrivia && (ctx._cstBuf !== void 0 || ctx._cstTriviaLog !== void 0)) {
1419
- if (mask === void 0 || kinds === void 0 || (mask & 1 << ch.kindIndex) !== 0) {
1420
- pushCstTriviaEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : void 0);
1445
+ if (mask === void 0 || kinds === void 0 || (mask & 1 << ch2.kindIndex) !== 0) {
1446
+ pushCstTriviaEntry(ctx, ch2.start, ch2.end, kinds ? ch2.kindIndex : void 0);
1421
1447
  }
1422
1448
  }
1423
1449
  }
@@ -2193,6 +2219,12 @@ function rules(a, b) {
2193
2219
  if (rule && !rule._meta.isTrivia) rule._meta.grammarTrivia = options.trivia;
2194
2220
  }
2195
2221
  }
2222
+ if (options?.scanSkip != null) {
2223
+ for (const key of Object.keys(definitions)) {
2224
+ const rule = cache[key];
2225
+ if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
2226
+ }
2227
+ }
2196
2228
  for (const key of Object.keys(definitions)) {
2197
2229
  markUnusedValues(cache[key]);
2198
2230
  }
@@ -2675,9 +2707,11 @@ function parse(combinator, input, opts = {}) {
2675
2707
  const _errors = opts.recover ? [] : void 0;
2676
2708
  const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
2677
2709
  const grammarTrivia = combinator._meta.grammarTrivia;
2710
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
2678
2711
  const ctx = {
2679
2712
  trackLines,
2680
2713
  ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...grammarTrivia._meta.triviaKindLabels ? { triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {} } : {},
2714
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
2681
2715
  ..._errors !== void 0 ? { _errors } : {},
2682
2716
  ..._probe !== void 0 ? { _probe } : {}
2683
2717
  };
@@ -2786,6 +2820,413 @@ function leaf(root, fn) {
2786
2820
  };
2787
2821
  }
2788
2822
 
2823
+ // src/version.ts
2824
+ var PARSEMAN_VERSION = "0.32.0";
2825
+
2826
+ // src/analysis/gating.ts
2827
+ var isAny = (fs) => fs.kind === "any";
2828
+ function intersects2(a, b) {
2829
+ if (a.kind === "any" || b.kind === "any") return true;
2830
+ if (a.kind === "empty" || b.kind === "empty") return false;
2831
+ for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
2832
+ return false;
2833
+ }
2834
+ function intersection(a, b) {
2835
+ if (a.kind === "any") return b;
2836
+ if (b.kind === "any") return a;
2837
+ if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
2838
+ const ranges = [];
2839
+ for (const ra of a.ranges) for (const rb of b.ranges) {
2840
+ const lo = Math.max(ra.lo, rb.lo);
2841
+ const hi = Math.min(ra.hi, rb.hi);
2842
+ if (lo <= hi) ranges.push({ lo, hi });
2843
+ }
2844
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
2845
+ }
2846
+ function combine(sets) {
2847
+ let acc = { kind: "empty" };
2848
+ for (const s of sets) {
2849
+ if (s.kind === "any") return { kind: "any" };
2850
+ if (s.kind === "empty") continue;
2851
+ acc = acc.kind === "ranges" ? { kind: "ranges", ranges: [...acc.ranges, ...s.ranges] } : s;
2852
+ }
2853
+ return acc;
2854
+ }
2855
+ var ch = (c) => c >= 32 && c < 127 ? `'${String.fromCharCode(c)}'` : `\\u${c}`;
2856
+ function firstSetToString(fs) {
2857
+ if (fs.kind === "any") return "ANY";
2858
+ if (fs.kind === "empty") return "(empty)";
2859
+ return fs.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
2860
+ }
2861
+ var ruleNameOf = (p) => p._ruleName;
2862
+ var SUGGESTIONS = {
2863
+ "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.",
2864
+ "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.",
2865
+ "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.",
2866
+ "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.",
2867
+ "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2868
+ "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2869
+ };
2870
+ function classifyBroadArm(arm) {
2871
+ const seen = /* @__PURE__ */ new Set();
2872
+ const walk2 = (p) => {
2873
+ if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2874
+ seen.add(p);
2875
+ const d = p._def;
2876
+ switch (d.tag) {
2877
+ case "literal":
2878
+ case "regex":
2879
+ case "keywords":
2880
+ return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2881
+ case "not":
2882
+ return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
2883
+ case "scanTo":
2884
+ return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2885
+ // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
2886
+ // its own — it is the poison. `withCtx`/`recover` FORWARD their inner
2887
+ // first-set (see firstSetOf), so a broad result comes from the inner parser:
2888
+ // walk into it for a precise cause.
2889
+ case "guard":
2890
+ return { cause: "opaque-wrapper", detail: `opaque wrapper (${d.tag})` };
2891
+ // attempt/withCtx/recover FORWARD their inner first-set (transparent for
2892
+ // dispatch), so a broad result comes from the inner parser — walk into it.
2893
+ case "attempt":
2894
+ case "withCtx":
2895
+ case "recover":
2896
+ return walk2(d.parser);
2897
+ case "lazy": {
2898
+ const name = p._ruleName;
2899
+ try {
2900
+ const inner = walk2(d.thunk());
2901
+ if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
2902
+ return inner;
2903
+ } catch {
2904
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
2905
+ }
2906
+ }
2907
+ case "optional":
2908
+ case "many": {
2909
+ const inner = walk2(d.parser);
2910
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
2911
+ }
2912
+ case "oneOrMore":
2913
+ case "transform":
2914
+ case "label":
2915
+ case "field":
2916
+ case "trivia":
2917
+ case "token":
2918
+ case "leaf":
2919
+ case "node":
2920
+ case "grammar":
2921
+ case "expect":
2922
+ return walk2(d.parser);
2923
+ case "skip":
2924
+ return walk2(d.main);
2925
+ case "sequence": {
2926
+ let sawNullablePrefix = false;
2927
+ for (const t of d.parsers) {
2928
+ if (t._def.tag === "not") {
2929
+ sawNullablePrefix = true;
2930
+ continue;
2931
+ }
2932
+ if (isAny(firstSetOf(t))) {
2933
+ const inner = walk2(t);
2934
+ if (sawNullablePrefix)
2935
+ return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
2936
+ return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2937
+ }
2938
+ if (matchesEmpty(t)) {
2939
+ sawNullablePrefix = true;
2940
+ continue;
2941
+ }
2942
+ return null;
2943
+ }
2944
+ return { cause: "broad-recognizer", detail: "sequence of only nullable/zero-width terms" };
2945
+ }
2946
+ case "choice": {
2947
+ for (const a of d.parsers) {
2948
+ const r = walk2(a);
2949
+ if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
2950
+ }
2951
+ return null;
2952
+ }
2953
+ default:
2954
+ return { cause: "broad-recognizer", detail: `unmodeled construct (${d.tag})` };
2955
+ }
2956
+ };
2957
+ return walk2(arm) ?? { cause: "broad-recognizer", detail: "broad (cause not localized)" };
2958
+ }
2959
+ function peelToLeading(arm) {
2960
+ let d = arm._def;
2961
+ let cur = arm;
2962
+ for (; ; ) {
2963
+ if (d.tag === "node" || d.tag === "grammar" || d.tag === "transform" || d.tag === "label" || d.tag === "attempt") {
2964
+ cur = d.parser;
2965
+ d = cur._def;
2966
+ continue;
2967
+ }
2968
+ break;
2969
+ }
2970
+ if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
2971
+ return cur;
2972
+ }
2973
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
2974
+ function detectAntiPatterns(rule, arms) {
2975
+ const out = [];
2976
+ arms.forEach((arm, i) => {
2977
+ const lead = peelToLeading(arm);
2978
+ const ld = lead._def;
2979
+ if (ld.tag === "not") {
2980
+ const inner = ld.parser;
2981
+ if (inner._def.tag === "not") {
2982
+ out.push({
2983
+ kind: "double-not",
2984
+ rule,
2985
+ armIndex: i,
2986
+ 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."
2987
+ });
2988
+ } else {
2989
+ out.push({
2990
+ kind: "leading-not",
2991
+ rule,
2992
+ armIndex: i,
2993
+ 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."
2994
+ });
2995
+ }
2996
+ }
2997
+ if (ld.tag === "regex" && KEYWORD_REGEX_RE.test(ld.source)) {
2998
+ out.push({
2999
+ kind: "keyword-regex",
3000
+ rule,
3001
+ armIndex: i,
3002
+ 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).`
3003
+ });
3004
+ }
3005
+ });
3006
+ return out;
3007
+ }
3008
+ function analyzeGating(entry, opts) {
3009
+ const raw = [];
3010
+ const antiPatterns = [];
3011
+ const visited = /* @__PURE__ */ new Set();
3012
+ const visit = (p, enclosingRule) => {
3013
+ if (visited.has(p)) return;
3014
+ visited.add(p);
3015
+ const d = p._def;
3016
+ const rule = ruleNameOf(p) ?? enclosingRule;
3017
+ if (d.tag === "choice") {
3018
+ raw.push({ g: analyzeChoice(p, d, rule), rule });
3019
+ antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3020
+ }
3021
+ const rec = d;
3022
+ const kids = [];
3023
+ if (Array.isArray(rec.parsers)) kids.push(...rec.parsers);
3024
+ for (const k of ["parser", "main", "skipped", "separator", "sentinel"])
3025
+ if (rec[k]) kids.push(rec[k]);
3026
+ if (Array.isArray(rec.skip)) kids.push(...rec.skip);
3027
+ if (d.tag === "lazy") {
3028
+ try {
3029
+ kids.push(d.thunk());
3030
+ } catch {
3031
+ }
3032
+ }
3033
+ for (const k of kids) visit(k, rule);
3034
+ };
3035
+ visit(entry, ruleNameOf(entry) ?? "<entry>");
3036
+ const perRule = /* @__PURE__ */ new Map();
3037
+ for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3038
+ const seenInRule = /* @__PURE__ */ new Map();
3039
+ const accept = new Set(opts?.accept ?? []);
3040
+ const usedAccept = /* @__PURE__ */ new Set();
3041
+ const choices = raw.map(({ g, rule }) => {
3042
+ const n = seenInRule.get(rule) ?? 0;
3043
+ seenInRule.set(rule, n + 1);
3044
+ const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
3045
+ const isAccepted = g.gates === "no" && accept.has(id);
3046
+ if (isAccepted) usedAccept.add(id);
3047
+ return { ...g, id, accepted: isAccepted };
3048
+ });
3049
+ const gated = choices.filter((c) => c.gates === "yes").length;
3050
+ const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3051
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3052
+ const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3053
+ const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3054
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3055
+ }
3056
+ function analyzeChoice(p, d, rule) {
3057
+ const arms = d.parsers;
3058
+ const shallow = arms.map((a) => a._meta.firstSet);
3059
+ const deep = arms.map((a) => firstSetOf(a));
3060
+ const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3061
+ const { cause, detail } = classifyBroadArm(arms[index]);
3062
+ return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3063
+ });
3064
+ const overlaps = [];
3065
+ for (let i = 0; i < deep.length; i++)
3066
+ for (let j = i + 1; j < deep.length; j++)
3067
+ if (!isAny(deep[i]) && !isAny(deep[j]) && intersects2(deep[i], deep[j]))
3068
+ overlaps.push({
3069
+ a: i,
3070
+ b: j,
3071
+ on: intersection(deep[i], deep[j]),
3072
+ 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."
3073
+ });
3074
+ let gates;
3075
+ if (d.disjoint) gates = "yes";
3076
+ else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3077
+ else gates = "no";
3078
+ return {
3079
+ rule,
3080
+ strategy: d.strategy?.tag ?? "firstMatch",
3081
+ gates,
3082
+ combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3083
+ anyArms,
3084
+ overlaps
3085
+ };
3086
+ }
3087
+ function formatGatingWarnings(report) {
3088
+ const lines = [];
3089
+ for (const c of report.ungated) {
3090
+ lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
3091
+ for (const a of c.anyArms)
3092
+ lines.push(` \xB7 arm[${a.index}] first-set ANY (${a.cause}): ${a.detail}
3093
+ fix: ${a.suggestion}`);
3094
+ for (const o of c.overlaps)
3095
+ lines.push(` \xB7 arm[${o.a}] \u2229 arm[${o.b}] overlap on ${firstSetToString(o.on)}
3096
+ fix: ${o.suggestion}`);
3097
+ lines.push(` (intentional? accept it in the gating snapshot: { accept: ['${c.id}'] }.)`);
3098
+ }
3099
+ for (const ap of report.antiPatterns)
3100
+ lines.push(`parseman anti-pattern [${ap.kind}] @ ${ap.rule} arm[${ap.armIndex}]: ${ap.message}`);
3101
+ return lines;
3102
+ }
3103
+
3104
+ // src/combinators/scanTo.ts
3105
+ function resolveScanSkip(explicitSkip, raw, ctx) {
3106
+ if (raw) return [];
3107
+ const trivia2 = ctx.trivia;
3108
+ const ambient = ctx.scanSkip;
3109
+ if (!trivia2 && !ambient) return explicitSkip;
3110
+ const out = [];
3111
+ if (trivia2) out.push(trivia2);
3112
+ if (ambient) out.push(...ambient);
3113
+ out.push(...explicitSkip);
3114
+ return out;
3115
+ }
3116
+ function scanTo(sentinel, { skip: skip2, raw = false, orEOF = false } = {}) {
3117
+ const meta = {
3118
+ firstSet: any(),
3119
+ canMatchNewline: true,
3120
+ isTrivia: false
3121
+ };
3122
+ const explicitSkip = skip2 ?? [];
3123
+ return {
3124
+ _tag: "scanTo",
3125
+ _meta: meta,
3126
+ _def: { tag: "scanTo", sentinel, skip: explicitSkip, raw, orEOF },
3127
+ parse(input, pos, ctx) {
3128
+ let cur = pos;
3129
+ const skip3 = resolveScanSkip(explicitSkip, raw, ctx);
3130
+ const probeCtx = {
3131
+ trackLines: ctx.trackLines,
3132
+ state: ctx.state,
3133
+ ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
3134
+ };
3135
+ const emit2 = (end) => {
3136
+ if (end > pos && cstCaptureActive(ctx)) {
3137
+ const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
3138
+ pushCstLeaf(ctx, leaf2);
3139
+ }
3140
+ };
3141
+ while (cur < input.length) {
3142
+ const s = sentinel.parse(input, cur, probeCtx);
3143
+ if (s.ok) {
3144
+ emit2(cur);
3145
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3146
+ }
3147
+ let advanced = false;
3148
+ for (const skipper of skip3) {
3149
+ const r = skipper.parse(input, cur, probeCtx);
3150
+ if (r.ok && r.span.end > cur) {
3151
+ cur = r.span.end;
3152
+ advanced = true;
3153
+ break;
3154
+ }
3155
+ }
3156
+ if (!advanced) cur++;
3157
+ }
3158
+ if (orEOF) {
3159
+ emit2(cur);
3160
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
3161
+ }
3162
+ const sentDef = sentinel._def;
3163
+ const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
3164
+ return { ok: false, expected, span: { start: pos, end: cur } };
3165
+ }
3166
+ };
3167
+ }
3168
+ function balanced(open, close, options = {}) {
3169
+ const ownSkip = options.skip ?? [];
3170
+ const combi = buildBalancedInterior(open, close, ownSkip);
3171
+ if (options.raw) return combi;
3172
+ const eagerParse = combi.parse.bind(combi);
3173
+ const cache = /* @__PURE__ */ new Map();
3174
+ combi._balancedAmbient = { open, close, ownSkip };
3175
+ combi.parse = (input, pos, ctx) => {
3176
+ const amb = ctx.scanSkip;
3177
+ if (!amb || amb.length === 0) return eagerParse(input, pos, ctx);
3178
+ let interior = cache.get(amb);
3179
+ if (!interior) {
3180
+ interior = buildBalancedInterior(open, close, [...amb, ...ownSkip]);
3181
+ cache.set(amb, interior);
3182
+ }
3183
+ return interior.parse(input, pos, ctx);
3184
+ };
3185
+ return combi;
3186
+ }
3187
+ function buildBalancedInterior(open, close, skips) {
3188
+ const self = ref();
3189
+ const stop = /* @__PURE__ */ new Set([open, close]);
3190
+ let bounded = true;
3191
+ for (const sk of skips) {
3192
+ const cs = firstSetClassChars(sk);
3193
+ if (cs === null) {
3194
+ bounded = false;
3195
+ break;
3196
+ }
3197
+ for (const ch2 of cs) stop.add(ch2);
3198
+ }
3199
+ const cls = [...stop].map(escapeClassChar).join("");
3200
+ const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
3201
+ const inner = many(choice(self, ...skips, content));
3202
+ const combi = transform(
3203
+ sequence(literal(open), inner, expect(literal(close))),
3204
+ // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
3205
+ // ParseError (recovered close). `c` is the close string or a ParseError.
3206
+ ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
3207
+ );
3208
+ if (combi._def.tag === "transform") {
3209
+ combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
3210
+ combi._def.recognitionOnly = true;
3211
+ }
3212
+ self.define(combi);
3213
+ return combi;
3214
+ }
3215
+ function firstSetClassChars(c) {
3216
+ const fs = c._meta.firstSet;
3217
+ if (fs.kind === "empty") return [];
3218
+ if (fs.kind !== "ranges") return null;
3219
+ const out = [];
3220
+ for (const { lo, hi } of fs.ranges) {
3221
+ if (hi - lo > 8) return null;
3222
+ for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
3223
+ }
3224
+ return out;
3225
+ }
3226
+ function escapeClassChar(ch2) {
3227
+ return ch2.replace(/[\\\]^-]/g, "\\$&");
3228
+ }
3229
+
2789
3230
  // src/compiler/grammar-coverage-ids.ts
2790
3231
  function children(def, winners) {
2791
3232
  switch (def.tag) {
@@ -2898,16 +3339,16 @@ function literalCodePoints(frag) {
2898
3339
  const out = [];
2899
3340
  let i = 0;
2900
3341
  while (i < frag.length) {
2901
- const ch = frag[i];
2902
- if (ch === "\\") {
3342
+ const ch2 = frag[i];
3343
+ if (ch2 === "\\") {
2903
3344
  const e = frag[i + 1];
2904
3345
  if (e === void 0) return null;
2905
3346
  out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e] : e.codePointAt(0));
2906
3347
  i += 2;
2907
3348
  continue;
2908
3349
  }
2909
- if (META.has(ch)) return null;
2910
- out.push(ch.codePointAt(0));
3350
+ if (META.has(ch2)) return null;
3351
+ out.push(ch2.codePointAt(0));
2911
3352
  i += 1;
2912
3353
  }
2913
3354
  return out.length ? out : null;
@@ -2986,8 +3427,8 @@ function parseSeqParts(source) {
2986
3427
  };
2987
3428
  let i = 0;
2988
3429
  while (i < source.length) {
2989
- const ch = source[i];
2990
- if (ch === "[") {
3430
+ const ch2 = source[i];
3431
+ if (ch2 === "[") {
2991
3432
  let k = i + 1;
2992
3433
  const neg = source[k] === "^";
2993
3434
  if (neg) k++;
@@ -3009,7 +3450,7 @@ function parseSeqParts(source) {
3009
3450
  i += runFrom(ranges, neg, i);
3010
3451
  continue;
3011
3452
  }
3012
- if (ch === "\\") {
3453
+ if (ch2 === "\\") {
3013
3454
  const e = source[i + 1];
3014
3455
  if (e === "d" || e === "w" || e === "s") {
3015
3456
  flush();
@@ -3038,7 +3479,7 @@ function parseSeqParts(source) {
3038
3479
  } else lit.push(cp2);
3039
3480
  continue;
3040
3481
  }
3041
- if (ch === "(") {
3482
+ if (ch2 === "(") {
3042
3483
  if (source[i + 1] !== "?" || source[i + 2] !== ":") return null;
3043
3484
  let k = i + 3;
3044
3485
  let depth = 1;
@@ -3087,9 +3528,9 @@ function parseSeqParts(source) {
3087
3528
  }
3088
3529
  continue;
3089
3530
  }
3090
- if (META.has(ch)) return null;
3091
- const cp = ch.codePointAt(0);
3092
- i += ch.length;
3531
+ if (META.has(ch2)) return null;
3532
+ const cp = ch2.codePointAt(0);
3533
+ i += ch2.length;
3093
3534
  if (source[i] === "?") {
3094
3535
  flush();
3095
3536
  parts.push({ part: "lit", cps: [cp], optional: true });
@@ -3266,12 +3707,12 @@ function unwrapOuterGroup(source) {
3266
3707
  let i = 3;
3267
3708
  let ok = true;
3268
3709
  while (i < source.length && depth > 0) {
3269
- const ch = source[i];
3270
- if (ch === "\\") {
3710
+ const ch2 = source[i];
3711
+ if (ch2 === "\\") {
3271
3712
  i += 2;
3272
3713
  continue;
3273
3714
  }
3274
- if (ch === "[") {
3715
+ if (ch2 === "[") {
3275
3716
  let j = i + 1;
3276
3717
  if (source[j] === "^") j++;
3277
3718
  while (j < source.length && source[j] !== "]") {
@@ -3285,8 +3726,8 @@ function unwrapOuterGroup(source) {
3285
3726
  i = j + 1;
3286
3727
  continue;
3287
3728
  }
3288
- if (ch === "(") depth++;
3289
- else if (ch === ")") depth--;
3729
+ if (ch2 === "(") depth++;
3730
+ else if (ch2 === ")") depth--;
3290
3731
  i++;
3291
3732
  }
3292
3733
  if (!ok || depth !== 0 || i !== source.length) break;
@@ -3301,12 +3742,12 @@ function splitTopLevelAlternation(source) {
3301
3742
  let last = 0;
3302
3743
  let i = 0;
3303
3744
  while (i < body.length) {
3304
- const ch = body[i];
3305
- if (ch === "\\") {
3745
+ const ch2 = body[i];
3746
+ if (ch2 === "\\") {
3306
3747
  i += 2;
3307
3748
  continue;
3308
3749
  }
3309
- if (ch === "[") {
3750
+ if (ch2 === "[") {
3310
3751
  let j = i + 1;
3311
3752
  if (body[j] === "^") j++;
3312
3753
  while (j < body.length && body[j] !== "]") {
@@ -3317,18 +3758,18 @@ function splitTopLevelAlternation(source) {
3317
3758
  i = j + 1;
3318
3759
  continue;
3319
3760
  }
3320
- if (ch === "(") {
3761
+ if (ch2 === "(") {
3321
3762
  depth++;
3322
3763
  i++;
3323
3764
  continue;
3324
3765
  }
3325
- if (ch === ")") {
3766
+ if (ch2 === ")") {
3326
3767
  depth--;
3327
3768
  if (depth < 0) return null;
3328
3769
  i++;
3329
3770
  continue;
3330
3771
  }
3331
- if (ch === "|" && depth === 0) {
3772
+ if (ch2 === "|" && depth === 0) {
3332
3773
  parts.push(body.slice(last, i));
3333
3774
  last = i + 1;
3334
3775
  i++;
@@ -3447,12 +3888,12 @@ function splitDelimArms(body) {
3447
3888
  let last = 0;
3448
3889
  let i = 0;
3449
3890
  while (i < body.length) {
3450
- const ch = body[i];
3451
- if (ch === "\\") {
3891
+ const ch2 = body[i];
3892
+ if (ch2 === "\\") {
3452
3893
  i += 2;
3453
3894
  continue;
3454
3895
  }
3455
- if (ch === "[") {
3896
+ if (ch2 === "[") {
3456
3897
  let j = i + 1;
3457
3898
  if (body[j] === "^") j++;
3458
3899
  while (j < body.length && body[j] !== "]") {
@@ -3463,18 +3904,18 @@ function splitDelimArms(body) {
3463
3904
  i = j + 1;
3464
3905
  continue;
3465
3906
  }
3466
- if (ch === "(") {
3907
+ if (ch2 === "(") {
3467
3908
  depth++;
3468
3909
  i++;
3469
3910
  continue;
3470
3911
  }
3471
- if (ch === ")") {
3912
+ if (ch2 === ")") {
3472
3913
  depth--;
3473
3914
  if (depth < 0) return null;
3474
3915
  i++;
3475
3916
  continue;
3476
3917
  }
3477
- if (ch === "|" && depth === 0) {
3918
+ if (ch2 === "|" && depth === 0) {
3478
3919
  arms.push(body.slice(last, i));
3479
3920
  last = i + 1;
3480
3921
  }
@@ -3526,12 +3967,12 @@ function parseDelimited(source) {
3526
3967
  let k = i + 3;
3527
3968
  let depth = 1;
3528
3969
  while (k < source.length && depth > 0) {
3529
- const ch = source[k];
3530
- if (ch === "\\") {
3970
+ const ch2 = source[k];
3971
+ if (ch2 === "\\") {
3531
3972
  k += 2;
3532
3973
  continue;
3533
3974
  }
3534
- if (ch === "[") {
3975
+ if (ch2 === "[") {
3535
3976
  let j = k + 1;
3536
3977
  if (source[j] === "^") j++;
3537
3978
  while (j < source.length && source[j] !== "]") {
@@ -3542,8 +3983,8 @@ function parseDelimited(source) {
3542
3983
  k = j + 1;
3543
3984
  continue;
3544
3985
  }
3545
- if (ch === "(") depth++;
3546
- else if (ch === ")") depth--;
3986
+ if (ch2 === "(") depth++;
3987
+ else if (ch2 === ")") depth--;
3547
3988
  k++;
3548
3989
  }
3549
3990
  if (depth !== 0) return null;
@@ -4215,58 +4656,88 @@ function tryInlineDestructureTransform(src, valueVars) {
4215
4656
 
4216
4657
  // src/compiler/codegen.ts
4217
4658
  function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
4218
- if (seen.has(p)) return { concrete: empty(), refs: [] };
4219
- seen.add(p);
4220
4659
  const d = p._def;
4660
+ if (d.tag === "lazy") {
4661
+ const name = p._ruleName;
4662
+ if (name !== void 0) return { alts: [[{ set: empty(), nullable: false, ref: name }]] };
4663
+ }
4664
+ if (seen.has(p)) return { alts: [[]] };
4665
+ seen.add(p);
4221
4666
  const rec = (c) => leadingFirstSetRecipe(c, seen);
4222
- const merge2 = (a, b) => ({ concrete: union(a.concrete, b.concrete), refs: [...a.refs, ...b.refs] });
4223
- switch (d.tag) {
4224
- case "lazy": {
4225
- const name = p._ruleName;
4226
- if (name !== void 0) return { concrete: empty(), refs: [name] };
4227
- try {
4228
- return rec(d.thunk());
4229
- } catch {
4230
- return { concrete: any(), refs: [] };
4667
+ const seg = (set, nullable2) => ({ set, nullable: nullable2 });
4668
+ const forceNullable = (r) => ({ alts: r.alts.map((alt) => alt.map((s) => ({ ...s, nullable: true }))) });
4669
+ const collapse = (r, nullable2) => {
4670
+ let set = empty();
4671
+ let hasRef = false;
4672
+ for (const alt of r.alts) {
4673
+ for (const s of alt) {
4674
+ if (s.ref !== void 0) hasRef = true;
4675
+ set = union(set, s.set);
4676
+ if (!s.nullable) break;
4231
4677
  }
4232
4678
  }
4233
- case "literal":
4234
- case "regex":
4235
- case "keywords":
4236
- return { concrete: p._meta.firstSet, refs: [] };
4237
- case "choice": {
4238
- let out = { concrete: empty(), refs: [] };
4239
- for (const arm of d.parsers) out = merge2(out, rec(arm));
4240
- return out;
4241
- }
4242
- case "sequence": {
4243
- let out = { concrete: empty(), refs: [] };
4244
- for (const term of d.parsers) {
4245
- out = merge2(out, rec(term));
4246
- if (!matchesEmpty(term)) return out;
4679
+ return seg(hasRef ? any() : set, nullable2);
4680
+ };
4681
+ const compute = () => {
4682
+ switch (d.tag) {
4683
+ case "lazy": {
4684
+ try {
4685
+ return rec(d.thunk());
4686
+ } catch {
4687
+ return { alts: [[seg(any(), false)]] };
4688
+ }
4247
4689
  }
4248
- return out;
4690
+ case "literal":
4691
+ case "regex":
4692
+ case "keywords":
4693
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4694
+ case "choice": {
4695
+ const alts = [];
4696
+ for (const arm of d.parsers) alts.push(...rec(arm).alts);
4697
+ return { alts };
4698
+ }
4699
+ case "sequence": {
4700
+ const chain = [];
4701
+ for (const term of d.parsers) {
4702
+ if (!isZeroWidthAssertion(term)) {
4703
+ const tr = rec(term);
4704
+ if (tr.alts.length === 1) chain.push(...tr.alts[0]);
4705
+ else chain.push(collapse(tr, matchesEmpty(term)));
4706
+ }
4707
+ if (!matchesEmpty(term)) return { alts: [chain] };
4708
+ }
4709
+ return { alts: [chain] };
4710
+ }
4711
+ // `many`/`optional` are skippable as a whole → their leading chars start the
4712
+ // sequence but the chain must continue past them (force nullable). `oneOrMore`
4713
+ // requires at least one repetition, so it is NOT made nullable here.
4714
+ case "many":
4715
+ case "optional":
4716
+ return forceNullable(rec(d.parser));
4717
+ case "oneOrMore":
4718
+ case "transform":
4719
+ case "label":
4720
+ case "field":
4721
+ case "trivia":
4722
+ case "token":
4723
+ case "leaf":
4724
+ case "node":
4725
+ case "grammar":
4726
+ case "expect":
4727
+ return rec(d.parser);
4728
+ case "sepBy":
4729
+ return rec(d.parser);
4730
+ case "skip":
4731
+ return rec(d.main);
4732
+ // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4733
+ default:
4734
+ return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
4249
4735
  }
4250
- case "oneOrMore":
4251
- case "many":
4252
- case "optional":
4253
- case "transform":
4254
- case "label":
4255
- case "field":
4256
- case "trivia":
4257
- case "token":
4258
- case "leaf":
4259
- case "node":
4260
- case "grammar":
4261
- case "expect":
4262
- return rec(d.parser);
4263
- case "sepBy":
4264
- return rec(d.parser);
4265
- case "skip":
4266
- return rec(d.main);
4267
- // not / scanTo / guard / withCtx / recover / unknown → shallow set (safe).
4268
- default:
4269
- return { concrete: p._meta.firstSet, refs: [] };
4736
+ };
4737
+ try {
4738
+ return compute();
4739
+ } finally {
4740
+ seen.delete(p);
4270
4741
  }
4271
4742
  }
4272
4743
  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 }";
@@ -4462,16 +4933,19 @@ function ensureTriviaFn(ctx) {
4462
4933
  const savedIndent = ctx.indent;
4463
4934
  const savedFailLabel = ctx.failLabel;
4464
4935
  const savedTrivia = ctx.activeTrivia;
4936
+ const savedScanSkip = ctx.activeScanSkip;
4465
4937
  const savedCapAsTrivia = ctx.capAsTrivia;
4466
4938
  ctx.indent = 2;
4467
4939
  ctx.failLabel = "_triv";
4468
4940
  ctx.capAsTrivia = true;
4469
4941
  ctx.activeTrivia = void 0;
4942
+ ctx.activeScanSkip = void 0;
4470
4943
  const r = emit(trivia2, ctx, "_pos");
4471
4944
  ctx.indent = savedIndent;
4472
4945
  ctx.failLabel = savedFailLabel;
4473
4946
  ctx.capAsTrivia = savedCapAsTrivia;
4474
4947
  ctx.activeTrivia = savedTrivia;
4948
+ ctx.activeScanSkip = savedScanSkip;
4475
4949
  ctx.namedFnDecls.push([
4476
4950
  `function ${fnName}(input, _pos, _ctx, _cap) {`,
4477
4951
  ` let _e = _pos`,
@@ -4749,7 +5223,7 @@ function emitKeywordsFast(def, ctx, pos) {
4749
5223
  const bodyInd = ind(ctx) + " ";
4750
5224
  const tries = [];
4751
5225
  for (const w of def.words) {
4752
- const cps = Array.from(w, (ch) => ch.codePointAt(0));
5226
+ const cps = Array.from(w, (ch2) => ch2.codePointAt(0));
4753
5227
  let shape = def.caseInsensitive ? { kind: "litFold", open: cps } : { kind: "seq", parts: [{ part: "lit", cps, optional: false }] };
4754
5228
  if (boundary) {
4755
5229
  shape = { kind: "lookahead", inner: shape, ranges: boundary.ranges, classNegated: boundary.negated, negative: true };
@@ -4942,7 +5416,7 @@ function canMatchEmptyAtStart(p) {
4942
5416
  const d = p._def;
4943
5417
  switch (d.tag) {
4944
5418
  case "regex":
4945
- return /(?:[*?]|\{0,|\{\d*,)/.test(d.source);
5419
+ return regexMatchesEmpty(d.source, d.flags);
4946
5420
  case "optional":
4947
5421
  return true;
4948
5422
  case "many":
@@ -4994,10 +5468,10 @@ function emitDisjointArm(p, ctx, pos, valV, endV, coverageId) {
4994
5468
  }
4995
5469
  function emitDisjointArmGated(def, i, ctx, pos, valV, endV, coverageId) {
4996
5470
  const p = def.parsers[i];
4997
- const gate = def.gates[i];
4998
- if (!gate) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
5471
+ const gate2 = def.gates[i];
5472
+ if (!gate2) return emitDisjointArm(p, ctx, pos, valV, endV, coverageId);
4999
5473
  const gateIdx = ctx.mapFns.length;
5000
- ctx.mapFns.push(gate);
5474
+ ctx.mapFns.push(gate2);
5001
5475
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5002
5476
  const gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5003
5477
  const stmts = [`${ind(ctx)}if (${gateCond}) {`];
@@ -5155,7 +5629,7 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5155
5629
  if (preStmts) stmts.push(...preStmts);
5156
5630
  for (let i = 0; i < def.parsers.length; i++) {
5157
5631
  const p = def.parsers[i];
5158
- const gate = def.gates[i];
5632
+ const gate2 = def.gates[i];
5159
5633
  const autoNot = def.autoNot[i];
5160
5634
  const atStart = failsAtStart(p);
5161
5635
  const staticFx = armStaticExpected(ctx, p);
@@ -5167,9 +5641,9 @@ function emitFirstMatch(def, ctx, pos, coverageBase, preStmts, armGuardPrefix) {
5167
5641
  })();
5168
5642
  const fsGuard = armGuardPrefix === void 0 ? baseFsGuard : baseFsGuard === null ? armGuardPrefix : `${armGuardPrefix} && (${baseFsGuard})`;
5169
5643
  let gateCond = null;
5170
- if (gate) {
5644
+ if (gate2) {
5171
5645
  const gateIdx = ctx.mapFns.length;
5172
- ctx.mapFns.push(gate);
5646
+ ctx.mapFns.push(gate2);
5173
5647
  ctx.mapFnSrcs.push(def.gateSrcs?.[i] ?? null);
5174
5648
  gateCond = `${mfRef(ctx)}[${gateIdx}](_ctx.state)`;
5175
5649
  }
@@ -5586,10 +6060,15 @@ function emitScanTo(def, ctx, pos) {
5586
6060
  const probeCtx = { ...ctx, capturing: false, noHoist: true };
5587
6061
  const { stmts: sentStmts, okVar: sentOk } = emitFallible(def.sentinel, probeCtx, curV);
5588
6062
  stmts.push(...sentStmts, `${ind(ctx)}if (${sentOk}) { ${foundV} = true; break }`);
5589
- if (def.skip.length > 0) {
6063
+ const effectiveSkip = def.raw ? def.skip : [
6064
+ ...ctx.activeTrivia ? [ctx.activeTrivia] : [],
6065
+ ...ctx.activeScanSkip ?? [],
6066
+ ...def.skip
6067
+ ];
6068
+ if (effectiveSkip.length > 0) {
5590
6069
  const advV = v(ctx, "_stadv");
5591
6070
  stmts.push(`${ind(ctx)}let ${advV} = false`);
5592
- for (const skipper of def.skip) {
6071
+ for (const skipper of effectiveSkip) {
5593
6072
  const { stmts: skStmts, okVar: skOk, endVar: skEnd } = emitFallible(skipper, probeCtx, curV);
5594
6073
  stmts.push(
5595
6074
  `${ind(ctx)}if (!${advV}) {`,
@@ -6059,6 +6538,24 @@ function emit(p, ctx, pos) {
6059
6538
  return instrument(dispatch());
6060
6539
  }
6061
6540
  function emitDispatch(p, ctx, pos) {
6541
+ const bal = p._balancedAmbient;
6542
+ if (bal && ctx.activeScanSkip && ctx.activeScanSkip.length > 0 && !ctx.balancedRebuildStack?.has(p)) {
6543
+ const interior = buildBalancedInterior(bal.open, bal.close, [...ctx.activeScanSkip, ...bal.ownSkip]);
6544
+ markUnusedValues(interior);
6545
+ if (ctx.lazyUsage) {
6546
+ const u = analyzeLazyUsage(interior);
6547
+ for (const [k, n] of u.counts) ctx.lazyUsage.counts.set(k, (ctx.lazyUsage.counts.get(k) ?? 0) + n);
6548
+ for (const k of u.recursive) ctx.lazyUsage.recursive.add(k);
6549
+ for (const [k, s] of u.sizes) ctx.lazyUsage.sizes.set(k, s);
6550
+ }
6551
+ const stack = ctx.balancedRebuildStack ?? (ctx.balancedRebuildStack = /* @__PURE__ */ new Set());
6552
+ stack.add(p);
6553
+ try {
6554
+ return emit(interior, ctx, pos);
6555
+ } finally {
6556
+ stack.delete(p);
6557
+ }
6558
+ }
6062
6559
  const def = p._def;
6063
6560
  switch (def.tag) {
6064
6561
  case "literal":
@@ -6480,9 +6977,36 @@ function ruleDependencies(ruleMap) {
6480
6977
  }
6481
6978
  return deps;
6482
6979
  }
6980
+ function resolveGatingLevel(opt) {
6981
+ const explicit = typeof opt === "string" ? opt : opt?.level;
6982
+ if (explicit !== void 0) return explicit;
6983
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_GATING : void 0;
6984
+ if (env === "off" || env === "warn" || env === "error") return env;
6985
+ return "warn";
6986
+ }
6987
+ function runGatingDiagnostic(combinator, opt) {
6988
+ const level = resolveGatingLevel(opt);
6989
+ if (level === "off") return void 0;
6990
+ const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
6991
+ let report;
6992
+ try {
6993
+ report = analyzeGating(combinator, accept ? { accept } : void 0);
6994
+ } catch {
6995
+ return void 0;
6996
+ }
6997
+ const lines = formatGatingWarnings(report);
6998
+ if (lines.length > 0) {
6999
+ if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
7000
+ ${lines.join("\n")}`);
7001
+ for (const l of lines) console.warn(l);
7002
+ }
7003
+ return report;
7004
+ }
6483
7005
  function compile(combinator, mapFnSources, opts) {
7006
+ const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
6484
7007
  markUnusedValues(combinator);
6485
7008
  const grammarTrivia = combinator._meta.grammarTrivia;
7009
+ const grammarScanSkip = combinator._meta.grammarScanSkip;
6486
7010
  const ctx = {
6487
7011
  vars: 0,
6488
7012
  indent: 1,
@@ -6504,7 +7028,8 @@ function compile(combinator, mapFnSources, opts) {
6504
7028
  recovery: opts?.recovery ?? false,
6505
7029
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
6506
7030
  lazyUsage: analyzeLazyUsage(combinator),
6507
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7031
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7032
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6508
7033
  };
6509
7034
  const r = emit(combinator, ctx, "_pos");
6510
7035
  const coverageRootRuleId = ctx.coverage?.plan.rules.get(combinator);
@@ -6568,6 +7093,7 @@ function compile(combinator, mapFnSources, opts) {
6568
7093
  source,
6569
7094
  inlineExpression,
6570
7095
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
7096
+ ...gatingReport === void 0 ? {} : { gating: gatingReport },
6571
7097
  parse(input, pos = 0) {
6572
7098
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
6573
7099
  },
@@ -6603,6 +7129,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6603
7129
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
6604
7130
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
6605
7131
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7132
+ const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
6606
7133
  const ruleMap = ruleMapArg.filter(([, val]) => {
6607
7134
  const d = val._def;
6608
7135
  if (d.tag !== "lazy") return true;
@@ -6640,7 +7167,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
6640
7167
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
6641
7168
  ns,
6642
7169
  deferFirstSetRefs: true,
6643
- ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {}
7170
+ ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
7171
+ ...grammarScanSkip ? { activeScanSkip: grammarScanSkip } : {}
6644
7172
  };
6645
7173
  const seen = /* @__PURE__ */ new Map();
6646
7174
  const ruleNames = /* @__PURE__ */ new Map();
@@ -6704,9 +7232,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
6704
7232
  ctx.failLabel = "_pfail";
6705
7233
  ctx.recordFail = true;
6706
7234
  const savedTrivia = ctx.activeTrivia;
6707
- if (rule._meta.isTrivia) ctx.activeTrivia = void 0;
7235
+ const savedScanSkip = ctx.activeScanSkip;
7236
+ if (rule._meta.isTrivia) {
7237
+ ctx.activeTrivia = void 0;
7238
+ ctx.activeScanSkip = void 0;
7239
+ }
6708
7240
  const body = emit(resolved, ctx, "_pos");
6709
7241
  ctx.activeTrivia = savedTrivia;
7242
+ ctx.activeScanSkip = savedScanSkip;
6710
7243
  ctx.indent = savedIndent;
6711
7244
  ctx.failLabel = savedFail;
6712
7245
  ctx.recordFail = savedRec;
@@ -6753,6 +7286,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
6753
7286
  }
6754
7287
  const firstSets = /* @__PURE__ */ new Map();
6755
7288
  const firstSetRecipes = /* @__PURE__ */ new Map();
7289
+ const nullable2 = /* @__PURE__ */ new Map();
6756
7290
  for (const [key, rule] of ruleMap) {
6757
7291
  let resolved = rule;
6758
7292
  const d = rule._def;
@@ -6764,15 +7298,18 @@ function compileLinkable(ruleMapArg, ns, opts) {
6764
7298
  }
6765
7299
  }
6766
7300
  firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
6767
- firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { concrete: any(), refs: [] } : leadingFirstSetRecipe(resolved));
7301
+ nullable2.set(key, matchesEmpty(resolved));
7302
+ firstSetRecipes.set(key, canMatchEmptyAtStart(resolved) ? { alts: [[{ set: any(), nullable: false }]] } : leadingFirstSetRecipe(resolved));
6768
7303
  }
6769
7304
  return {
7305
+ v: PARSEMAN_VERSION,
6770
7306
  ns,
6771
7307
  keys: perEntry.map((e) => e.key),
6772
7308
  prelude,
6773
7309
  ruleFns,
6774
7310
  wrappers,
6775
7311
  firstSets,
7312
+ nullable: nullable2,
6776
7313
  firstSetRecipes,
6777
7314
  deps: ruleDependencies(ruleMap),
6778
7315
  needsEmptyTl: !!ctx.needsEmptyTl,
@@ -6851,101 +7388,6 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
6851
7388
  ].join("\n");
6852
7389
  }
6853
7390
 
6854
- // src/combinators/scanTo.ts
6855
- function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
6856
- const meta = {
6857
- firstSet: any(),
6858
- canMatchNewline: true,
6859
- isTrivia: false
6860
- };
6861
- return {
6862
- _tag: "scanTo",
6863
- _meta: meta,
6864
- _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
6865
- parse(input, pos, ctx) {
6866
- let cur = pos;
6867
- const probeCtx = {
6868
- trackLines: ctx.trackLines,
6869
- state: ctx.state,
6870
- ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
6871
- };
6872
- const emit2 = (end) => {
6873
- if (end > pos && cstCaptureActive(ctx)) {
6874
- const leaf2 = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
6875
- pushCstLeaf(ctx, leaf2);
6876
- }
6877
- };
6878
- while (cur < input.length) {
6879
- const s = sentinel.parse(input, cur, probeCtx);
6880
- if (s.ok) {
6881
- emit2(cur);
6882
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6883
- }
6884
- let advanced = false;
6885
- for (const skipper of skip2) {
6886
- const r = skipper.parse(input, cur, probeCtx);
6887
- if (r.ok && r.span.end > cur) {
6888
- cur = r.span.end;
6889
- advanced = true;
6890
- break;
6891
- }
6892
- }
6893
- if (!advanced) cur++;
6894
- }
6895
- if (orEOF) {
6896
- emit2(cur);
6897
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } };
6898
- }
6899
- const sentDef = sentinel._def;
6900
- const expected = sentDef.tag === "literal" ? [JSON.stringify(sentDef.value)] : ["sentinel"];
6901
- return { ok: false, expected, span: { start: pos, end: cur } };
6902
- }
6903
- };
6904
- }
6905
- function balanced(open, close, options = {}) {
6906
- const self = ref();
6907
- const skips = options.skip ?? [];
6908
- const stop = /* @__PURE__ */ new Set([open, close]);
6909
- let bounded = true;
6910
- for (const sk of skips) {
6911
- const cs = firstSetClassChars(sk);
6912
- if (cs === null) {
6913
- bounded = false;
6914
- break;
6915
- }
6916
- for (const ch of cs) stop.add(ch);
6917
- }
6918
- const cls = [...stop].map(escapeClassChar).join("");
6919
- const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
6920
- const inner = many(choice(self, ...skips, content));
6921
- const combi = transform(
6922
- sequence(literal(open), inner, expect(literal(close))),
6923
- // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
6924
- // ParseError (recovered close). `c` is the close string or a ParseError.
6925
- ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
6926
- );
6927
- if (combi._def.tag === "transform") {
6928
- combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
6929
- combi._def.recognitionOnly = true;
6930
- }
6931
- self.define(combi);
6932
- return combi;
6933
- }
6934
- function firstSetClassChars(c) {
6935
- const fs = c._meta.firstSet;
6936
- if (fs.kind === "empty") return [];
6937
- if (fs.kind !== "ranges") return null;
6938
- const out = [];
6939
- for (const { lo, hi } of fs.ranges) {
6940
- if (hi - lo > 8) return null;
6941
- for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
6942
- }
6943
- return out;
6944
- }
6945
- function escapeClassChar(ch) {
6946
- return ch.replace(/[\\\]^-]/g, "\\$&");
6947
- }
6948
-
6949
7391
  // src/combinators/withCtx.ts
6950
7392
  function withCtx(extra, combinator) {
6951
7393
  const meta = {
@@ -7002,7 +7444,7 @@ function childrenOf2(def) {
7002
7444
  return [];
7003
7445
  }
7004
7446
  }
7005
- var ruleNameOf = (c) => c._ruleName;
7447
+ var ruleNameOf2 = (c) => c._ruleName;
7006
7448
  function lazyTarget(c) {
7007
7449
  if (c._def.tag !== "lazy") return null;
7008
7450
  try {
@@ -7069,15 +7511,15 @@ function evalRuleMapIR(ir) {
7069
7511
  if (Array.isArray(it)) {
7070
7512
  const [src, comb] = it;
7071
7513
  gateSrcs.push(src);
7072
- let gate;
7514
+ let gate2;
7073
7515
  try {
7074
- gate = (0, eval)(`(${src})`);
7516
+ gate2 = (0, eval)(`(${src})`);
7075
7517
  } catch {
7076
- gate = () => {
7518
+ gate2 = () => {
7077
7519
  throw new Error("IR gate fn not materialized");
7078
7520
  };
7079
7521
  }
7080
- return { gate, combinator: comb };
7522
+ return { gate: gate2, combinator: comb };
7081
7523
  }
7082
7524
  gateSrcs.push(null);
7083
7525
  return it;
@@ -7184,7 +7626,7 @@ var Serializer = class {
7184
7626
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7185
7627
  * `g[name]` ref — only the top-level entry is unwrapped. */
7186
7628
  body(c) {
7187
- return c._def.tag === "lazy" && ruleNameOf(c) !== void 0 ? lazyTarget(c) ?? c : c;
7629
+ return c._def.tag === "lazy" && ruleNameOf2(c) !== void 0 ? lazyTarget(c) ?? c : c;
7188
7630
  }
7189
7631
  run() {
7190
7632
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
@@ -7209,7 +7651,7 @@ ${body}
7209
7651
  analyze(c, active) {
7210
7652
  const def = c._def;
7211
7653
  if (def.tag === "lazy") {
7212
- if (ruleNameOf(c) !== void 0) return;
7654
+ if (ruleNameOf2(c) !== void 0) return;
7213
7655
  const target = lazyTarget(c);
7214
7656
  if (!target) throw new Unserializable("unresolved unnamed ref");
7215
7657
  if (active.has(target)) {
@@ -7261,7 +7703,7 @@ ${body}
7261
7703
  const kid = (x) => this.ref(x, selfOf);
7262
7704
  switch (def.tag) {
7263
7705
  case "lazy": {
7264
- if (ruleNameOf(c) !== void 0) return `g[${JSON.stringify(ruleNameOf(c))}]`;
7706
+ if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
7265
7707
  const target = lazyTarget(c);
7266
7708
  if (selfOf && target === selfOf.comb) return selfOf.var;
7267
7709
  return this.ref(target, selfOf);
@@ -7315,7 +7757,7 @@ ${body}
7315
7757
  case "skip":
7316
7758
  return `skip(${kid(def.main)}, ${kid(def.skipped)})`;
7317
7759
  case "scanTo":
7318
- return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}], orEOF: ${def.orEOF} })`;
7760
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(", ")}]${def.raw ? ", raw: true" : ""}, orEOF: ${def.orEOF} })`;
7319
7761
  case "transform": {
7320
7762
  if (def.fnSrc === void 0) throw new Unserializable("transform without fnSrc");
7321
7763
  return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ", true" : ""})`;
@@ -7385,6 +7827,18 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
7385
7827
  }
7386
7828
  cstBuildHost._parsemanCstOutput = true;
7387
7829
  function fusedBody(pieces) {
7830
+ for (const p of pieces) {
7831
+ if (p.v === void 0) {
7832
+ throw new Error(
7833
+ `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.`
7834
+ );
7835
+ }
7836
+ if (p.v !== PARSEMAN_VERSION) {
7837
+ throw new Error(
7838
+ `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.`
7839
+ );
7840
+ }
7841
+ }
7388
7842
  const winner = /* @__PURE__ */ new Map();
7389
7843
  for (const p of pieces) for (const k of p.keys) winner.set(k, p);
7390
7844
  for (const [k, p] of winner) {
@@ -7410,23 +7864,38 @@ function fusedBody(pieces) {
7410
7864
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
7411
7865
  const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
7412
7866
  const ANY2 = { kind: "any" };
7867
+ const EMPTY3 = { kind: "empty" };
7413
7868
  const recipes = /* @__PURE__ */ new Map();
7414
7869
  const shallow = /* @__PURE__ */ new Map();
7870
+ const nullable2 = /* @__PURE__ */ new Map();
7415
7871
  for (const [k, p] of winner) {
7416
7872
  const r = p.firstSetRecipes?.get(k);
7417
7873
  if (r) recipes.set(k, r);
7418
7874
  const fs = p.firstSets?.get(k);
7419
7875
  if (fs) shallow.set(k, fs);
7420
- }
7876
+ const nu = p.nullable?.get(k);
7877
+ if (nu !== void 0) nullable2.set(k, nu);
7878
+ }
7879
+ const knownRef = (ref2) => recipes.has(ref2) || shallow.has(ref2);
7880
+ const refFS = (ref2) => knownRef(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2;
7881
+ const refNullable = (ref2) => knownRef(ref2) ? nullable2.get(ref2) ?? true : true;
7882
+ const resolveRecipe = (r) => {
7883
+ let out = EMPTY3;
7884
+ for (const chain of r.alts) {
7885
+ for (const seg of chain) {
7886
+ out = union(out, seg.ref !== void 0 ? refFS(seg.ref) : seg.set);
7887
+ const skippable = seg.ref !== void 0 ? seg.nullable || refNullable(seg.ref) : seg.nullable;
7888
+ if (!skippable) break;
7889
+ }
7890
+ }
7891
+ return out;
7892
+ };
7421
7893
  const finalFS = /* @__PURE__ */ new Map();
7422
- for (const [k] of winner) finalFS.set(k, recipes.get(k)?.concrete ?? shallow.get(k) ?? ANY2);
7894
+ for (const [k] of winner) finalFS.set(k, recipes.has(k) ? EMPTY3 : shallow.get(k) ?? ANY2);
7423
7895
  for (let iter = 0; iter <= recipes.size; iter++) {
7424
7896
  let changed = false;
7425
7897
  for (const [k, r] of recipes) {
7426
- let fs = r.concrete;
7427
- for (const ref2 of r.refs) {
7428
- fs = union(fs, recipes.has(ref2) || shallow.has(ref2) ? finalFS.get(ref2) ?? ANY2 : ANY2);
7429
- }
7898
+ const fs = resolveRecipe(r);
7430
7899
  if (JSON.stringify(finalFS.get(k)) !== JSON.stringify(fs)) {
7431
7900
  finalFS.set(k, fs);
7432
7901
  changed = true;
@@ -7558,8 +8027,8 @@ function composeLeaf(_items) {
7558
8027
  throw new Error("composeLeaf(): requires Parseman macro lowering; runtime composition is forbidden");
7559
8028
  }
7560
8029
 
7561
- // src/combinators/guard.ts
7562
- function guard(predicate) {
8030
+ // src/combinators/gate.ts
8031
+ function gate(predicate) {
7563
8032
  const meta = {
7564
8033
  firstSet: { kind: "any" },
7565
8034
  canMatchNewline: false,
@@ -7577,6 +8046,9 @@ function guard(predicate) {
7577
8046
  };
7578
8047
  }
7579
8048
 
8049
+ // src/combinators/guard.ts
8050
+ var guard = gate;
8051
+
7580
8052
  // src/combinators/completions.ts
7581
8053
  function completionsAt(target, input, offset, options = {}) {
7582
8054
  const probe = { offset, best: null };
@@ -8108,12 +8580,14 @@ function runOnce(entry, input, options, phase, profileState) {
8108
8580
  const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
8109
8581
  const skipGlobalSinks = phase === "recognizer" || phase === "capture";
8110
8582
  const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
8583
+ const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
8111
8584
  const ctx = {
8112
8585
  trackLines: false,
8113
8586
  ...skipGlobalSinks ? { _pmProfile: profile } : { _triviaLog: triviaLog, _errors: errors, ...profile === void 0 ? {} : { _pmProfile: profile } },
8114
8587
  build: options.build,
8115
8588
  state: options.state,
8116
8589
  ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...grammarTrivia._meta.triviaKindLabels ? { triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {} } : {},
8590
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
8117
8591
  ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
8118
8592
  ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
8119
8593
  ...options.instrumentation === void 0 ? {} : options.instrumentation
@@ -8399,7 +8873,7 @@ function coverageEntry(entry, collector, maps, trace) {
8399
8873
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
8400
8874
  }
8401
8875
  case "guard":
8402
- return guard(def.predicate);
8876
+ return gate(def.predicate);
8403
8877
  case "withCtx":
8404
8878
  return withCtx(def.extra, build(def.parser));
8405
8879
  case "expect":
@@ -8454,9 +8928,9 @@ function buildTriviaIndex(root, input = "", opts) {
8454
8928
  if (prev) merge(after, prev.span.end, run2);
8455
8929
  }
8456
8930
  }
8457
- const ch = node2.children;
8458
- if (ch) {
8459
- for (const c of ch) {
8931
+ const ch2 = node2.children;
8932
+ if (ch2) {
8933
+ for (const c of ch2) {
8460
8934
  if (typeof c === "object" && c !== null && c.children !== void 0) {
8461
8935
  visit(c);
8462
8936
  }
@@ -8767,6 +9241,7 @@ export {
8767
9241
  absoluteSpanCST,
8768
9242
  absolutize,
8769
9243
  absolutizeCST,
9244
+ analyzeGating,
8770
9245
  annotateSpan,
8771
9246
  applyEdit,
8772
9247
  attempt,
@@ -8791,8 +9266,11 @@ export {
8791
9266
  cstBuildHost,
8792
9267
  expect,
8793
9268
  field,
9269
+ firstSetToString,
9270
+ formatGatingWarnings,
8794
9271
  gapIsSignificant,
8795
9272
  gapText,
9273
+ gate,
8796
9274
  grammarCoverageDefinitions,
8797
9275
  guard,
8798
9276
  indentMixed,