parseman 0.32.0 → 0.34.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 (51) hide show
  1. package/dist/analysis/gating.d.ts +66 -1
  2. package/dist/analysis/gating.d.ts.map +1 -1
  3. package/dist/combinators/case-fold.d.ts +39 -0
  4. package/dist/combinators/case-fold.d.ts.map +1 -0
  5. package/dist/combinators/expect.d.ts.map +1 -1
  6. package/dist/combinators/first-set.d.ts +38 -11
  7. package/dist/combinators/first-set.d.ts.map +1 -1
  8. package/dist/combinators/keywords.d.ts +12 -3
  9. package/dist/combinators/keywords.d.ts.map +1 -1
  10. package/dist/combinators/not.d.ts.map +1 -1
  11. package/dist/combinators/peek.d.ts +27 -0
  12. package/dist/combinators/peek.d.ts.map +1 -0
  13. package/dist/combinators/repeat.d.ts +68 -3
  14. package/dist/combinators/repeat.d.ts.map +1 -1
  15. package/dist/combinators/trivia-skip.d.ts +23 -1
  16. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  17. package/dist/compiler/codegen.d.ts +40 -0
  18. package/dist/compiler/codegen.d.ts.map +1 -1
  19. package/dist/compiler/fields.d.ts.map +1 -1
  20. package/dist/compiler/ir-serialize.d.ts +1 -1
  21. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  22. package/dist/compiler/linker.d.ts +5 -0
  23. package/dist/compiler/linker.d.ts.map +1 -1
  24. package/dist/compiler/value-usage.d.ts.map +1 -1
  25. package/dist/coverage.d.ts.map +1 -1
  26. package/dist/index.cjs +459 -86
  27. package/dist/index.cjs.map +4 -4
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +459 -86
  31. package/dist/index.js.map +4 -4
  32. package/dist/language-service/index.cjs.map +1 -1
  33. package/dist/language-service/index.js.map +1 -1
  34. package/dist/plugin/evaluator.d.ts.map +1 -1
  35. package/dist/plugin/index.cjs +538 -123
  36. package/dist/plugin/index.cjs.map +4 -4
  37. package/dist/plugin/index.d.ts.map +1 -1
  38. package/dist/plugin/index.js +538 -123
  39. package/dist/plugin/index.js.map +4 -4
  40. package/dist/spec/ebnf.d.ts.map +1 -1
  41. package/dist/spec/index.cjs +79 -17
  42. package/dist/spec/index.cjs.map +2 -2
  43. package/dist/spec/index.js +79 -17
  44. package/dist/spec/index.js.map +2 -2
  45. package/dist/spec/model.d.ts +23 -3
  46. package/dist/spec/model.d.ts.map +1 -1
  47. package/dist/spec/railroad.d.ts.map +1 -1
  48. package/dist/types.d.ts +9 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/package.json +5 -1
package/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
1
  // src/combinators/first-set.ts
2
+ function resolveNamedRef(p, resolve) {
3
+ if (resolve === void 0) return void 0;
4
+ const name = p._ruleName;
5
+ return name === void 0 ? void 0 : resolve(name);
6
+ }
2
7
  function union(a, b) {
3
8
  if (a.kind === "any" || b.kind === "any") return { kind: "any" };
4
9
  if (a.kind === "empty") return b;
@@ -36,10 +41,10 @@ function any() {
36
41
  function empty() {
37
42
  return { kind: "empty" };
38
43
  }
39
- function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
44
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
40
45
  if (seen.has(p)) return true;
41
46
  seen.add(p);
42
- const me = (c) => matchesEmpty(c, seen);
47
+ const me = (c) => matchesEmpty(c, seen, resolve);
43
48
  const d = p._def;
44
49
  switch (d.tag) {
45
50
  case "literal":
@@ -56,8 +61,16 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
56
61
  case "many":
57
62
  case "optional":
58
63
  case "not":
64
+ case "peek":
59
65
  return true;
60
66
  // zero repetitions / absent / lookahead
67
+ // Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
68
+ // Any `min >= 1` requires that many ITEMS, so it is nullable only when the
69
+ // item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
70
+ // nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
71
+ // the gating diagnostic for a list that can never match empty.)
72
+ case "sepBy":
73
+ return d.min >= 1 ? me(d.parser) : true;
61
74
  case "oneOrMore":
62
75
  return me(d.parser);
63
76
  case "sequence":
@@ -81,7 +94,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
81
94
  try {
82
95
  return me(d.thunk());
83
96
  } catch {
84
- return true;
97
+ const t = resolveNamedRef(p, resolve);
98
+ return t ? me(t) : true;
85
99
  }
86
100
  default:
87
101
  return true;
@@ -90,18 +104,45 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
90
104
  function isZeroWidthAssertion(p) {
91
105
  return p._def.tag === "not";
92
106
  }
107
+ function isPositiveLookahead(p) {
108
+ return p._def.tag === "peek";
109
+ }
110
+ function narrowBy(acc, constraint) {
111
+ if (constraint.kind === "any") return acc;
112
+ if (acc === null) return constraint;
113
+ if (acc.kind === "any") return constraint;
114
+ if (acc.kind === "empty" || constraint.kind === "empty") return { kind: "empty" };
115
+ const ranges = [];
116
+ for (const a of acc.ranges) for (const b of constraint.ranges) {
117
+ const lo = Math.max(a.lo, b.lo);
118
+ const hi = Math.min(a.hi, b.hi);
119
+ if (lo <= hi) ranges.push({ lo, hi });
120
+ }
121
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
122
+ }
123
+ function applyAssertion(fs, assertion) {
124
+ if (assertion === null) return fs;
125
+ if (fs.kind === "empty") return assertion;
126
+ return narrowBy(fs, assertion) ?? fs;
127
+ }
93
128
  function sequenceFirstSet(parsers) {
94
129
  let fs = empty();
130
+ let assertion = null;
95
131
  for (const p of parsers) {
132
+ if (isPositiveLookahead(p)) {
133
+ assertion = narrowBy(assertion, p._meta.firstSet);
134
+ continue;
135
+ }
96
136
  if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet);
97
- if (!matchesEmpty(p)) return fs;
137
+ if (!matchesEmpty(p)) return applyAssertion(fs, assertion);
98
138
  }
99
- return fs;
139
+ return applyAssertion(fs, assertion);
100
140
  }
101
- function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
141
+ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve) {
102
142
  if (seen.has(p)) return any();
103
143
  seen.add(p);
104
- const fs = (c) => firstSetOf(c, seen);
144
+ const fs = (c) => firstSetOf(c, seen, resolve);
145
+ const empties = (c) => matchesEmpty(c, /* @__PURE__ */ new Set(), resolve);
105
146
  const d = p._def;
106
147
  switch (d.tag) {
107
148
  case "literal":
@@ -113,7 +154,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
113
154
  try {
114
155
  return fs(d.thunk());
115
156
  } catch {
116
- return any();
157
+ const t = resolveNamedRef(p, resolve);
158
+ return t ? fs(t) : any();
117
159
  }
118
160
  case "choice": {
119
161
  let out = empty();
@@ -122,11 +164,20 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
122
164
  }
123
165
  case "sequence": {
124
166
  let out = empty();
167
+ let assertion = null;
125
168
  for (const term of d.parsers) {
169
+ if (isPositiveLookahead(term)) {
170
+ assertion = narrowBy(assertion, fs(term));
171
+ continue;
172
+ }
126
173
  if (!isZeroWidthAssertion(term)) out = union(out, fs(term));
127
- if (!matchesEmpty(term)) return out;
174
+ if (!empties(term)) return applyAssertion(out, assertion);
128
175
  }
129
- return out;
176
+ return applyAssertion(out, assertion);
177
+ }
178
+ case "peek": {
179
+ const inner = d.parser;
180
+ return empties(inner) ? any() : fs(inner);
130
181
  }
131
182
  case "oneOrMore":
132
183
  case "many":
@@ -142,6 +193,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
142
193
  return fs(d.parser);
143
194
  case "sepBy":
144
195
  return fs(d.parser);
196
+ // both min 0 and min 1 start with the item
145
197
  case "skip":
146
198
  return fs(d.main);
147
199
  default:
@@ -826,9 +878,46 @@ function firstSetFromRegex(source) {
826
878
  return { firstSet: firstSet(ast), canMatchNewline: firstCanBeNewline(ast) };
827
879
  }
828
880
 
881
+ // src/combinators/case-fold.ts
882
+ function canonical(ch2) {
883
+ const u = ch2.toUpperCase();
884
+ if (u.length !== 1) return ch2;
885
+ if (ch2.codePointAt(0) >= 128 && u.codePointAt(0) < 128) return ch2;
886
+ return u;
887
+ }
888
+ var foldIndex = null;
889
+ function buildFoldIndex() {
890
+ const byCanonical = /* @__PURE__ */ new Map();
891
+ for (let cp = 0; cp < 65536; cp++) {
892
+ if (cp >= 55296 && cp <= 57343) continue;
893
+ const key = canonical(String.fromCharCode(cp));
894
+ const bucket = byCanonical.get(key);
895
+ if (bucket) bucket.push(cp);
896
+ else byCanonical.set(key, [cp]);
897
+ }
898
+ const index = /* @__PURE__ */ new Map();
899
+ for (const members of byCanonical.values()) {
900
+ if (members.length < 2) continue;
901
+ for (const cp of members) index.set(cp, members);
902
+ }
903
+ return index;
904
+ }
905
+ function caseFoldVariants(cp) {
906
+ if (cp < 128) {
907
+ if (cp >= 65 && cp <= 90) return [cp, cp + 32];
908
+ if (cp >= 97 && cp <= 122) return [cp, cp - 32];
909
+ return [cp];
910
+ }
911
+ if (cp > 65535) return [cp];
912
+ foldIndex ??= buildFoldIndex();
913
+ return foldIndex.get(cp) ?? [cp];
914
+ }
915
+
829
916
  // src/combinators/keywords.ts
830
- function word(str, boundary = "_0-9A-Za-z") {
831
- return keywords([str], { boundary });
917
+ function word(str, boundaryOrOpts, opts) {
918
+ const boundary = typeof boundaryOrOpts === "string" ? boundaryOrOpts : "_0-9A-Za-z";
919
+ const rest = typeof boundaryOrOpts === "object" && boundaryOrOpts !== null ? boundaryOrOpts : opts;
920
+ return keywords([str], { ...rest, boundary });
832
921
  }
833
922
  function makeWord(boundary = "_0-9A-Za-z") {
834
923
  return (str) => keywords([str], { boundary });
@@ -840,7 +929,7 @@ function keywords(words, opts = {}) {
840
929
  const sorted = [...new Set(words)].sort((a, b) => b.length - a.length);
841
930
  const alt = sorted.map(escapeRe).join("|");
842
931
  const boundary = opts.boundary ? `(?![${opts.boundary}])` : "";
843
- const flags = opts.caseInsensitive ? "iuy" : "uy";
932
+ const flags = opts.caseInsensitive ? "iy" : "uy";
844
933
  const re = new RegExp(`(?:${alt})${boundary}`, flags);
845
934
  let firstSet2 = empty();
846
935
  for (const w of sorted) {
@@ -848,10 +937,7 @@ function keywords(words, opts = {}) {
848
937
  const cp = w.codePointAt(0);
849
938
  firstSet2 = union(firstSet2, fromChar(cp));
850
939
  if (opts.caseInsensitive) {
851
- const u = String.fromCodePoint(cp).toUpperCase().codePointAt(0);
852
- const l = String.fromCodePoint(cp).toLowerCase().codePointAt(0);
853
- if (u !== void 0) firstSet2 = union(firstSet2, fromChar(u));
854
- if (l !== void 0) firstSet2 = union(firstSet2, fromChar(l));
940
+ for (const v2 of caseFoldVariants(cp)) firstSet2 = union(firstSet2, fromChar(v2));
855
941
  }
856
942
  }
857
943
  const meta = { firstSet: firstSet2, canMatchNewline: false, isTrivia: false };
@@ -1000,6 +1086,7 @@ function deriveExpected(c) {
1000
1086
  case "oneOrMore":
1001
1087
  case "transform":
1002
1088
  case "not":
1089
+ case "peek":
1003
1090
  return deriveExpected(def.parser);
1004
1091
  case "lazy": {
1005
1092
  const name = c._ruleName;
@@ -1473,6 +1560,13 @@ function rollbackTrivia(ctx, mark) {
1473
1560
  rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors });
1474
1561
  if (ctx._triviaLog) ctx._triviaLog.length = mark.log;
1475
1562
  }
1563
+ function saveLookaheadMark(ctx) {
1564
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null };
1565
+ }
1566
+ function rollbackLookahead(ctx, mark) {
1567
+ rollbackTrivia(ctx, mark);
1568
+ if (ctx._probe) ctx._probe.best = mark.probeBest;
1569
+ }
1476
1570
  function scanWithLabels(input, cur, ctx) {
1477
1571
  const triviaP = ctx.trivia;
1478
1572
  const spec = analyzeLabeledTrivia(triviaP);
@@ -1870,13 +1964,26 @@ function repItem(combinator, input, cur, ctx, guardable) {
1870
1964
  }
1871
1965
  return { value: result.value, end: result.span.end };
1872
1966
  }
1873
- function many(combinator) {
1967
+ function resolveBounds(what, opts) {
1968
+ const min = opts.min ?? 0;
1969
+ const max = opts.max ?? Infinity;
1970
+ const bad = (msg) => {
1971
+ throw new RangeError(`parseman: ${what} ${msg}`);
1972
+ };
1973
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`);
1974
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`);
1975
+ if (max < min) bad(`max (${max}) is less than min (${min}) \u2014 the combinator could never succeed`);
1976
+ return { min, max };
1977
+ }
1978
+ function many(combinator, opts = {}) {
1979
+ const { min, max } = resolveBounds("many()", opts);
1980
+ if (min >= 1) return atLeast(combinator, min, max);
1874
1981
  const meta = {
1875
1982
  firstSet: combinator._meta.firstSet,
1876
1983
  canMatchNewline: combinator._meta.canMatchNewline,
1877
1984
  isTrivia: false
1878
1985
  };
1879
- const def = { tag: "many", parser: combinator, min: 0 };
1986
+ const def = { tag: "many", parser: combinator, min: 0, ...max === Infinity ? {} : { max } };
1880
1987
  let expected;
1881
1988
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1882
1989
  return {
@@ -1886,7 +1993,9 @@ function many(combinator) {
1886
1993
  parse(input, pos, ctx) {
1887
1994
  const values = def.valueUnused ? void 0 : [];
1888
1995
  let cur = pos;
1996
+ let count = 0;
1889
1997
  while (cur < input.length) {
1998
+ if (count >= max) break;
1890
1999
  const item = repItem(combinator, input, cur, ctx, guardable);
1891
2000
  if (item === "stop") break;
1892
2001
  if ("fail" in item) {
@@ -1897,23 +2006,28 @@ function many(combinator) {
1897
2006
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1898
2007
  if (values !== void 0) values.push(error);
1899
2008
  captureError(ctx, error);
2009
+ count++;
1900
2010
  cur = end;
1901
2011
  continue;
1902
2012
  }
1903
2013
  if (values !== void 0) values.push(item.value);
2014
+ count++;
1904
2015
  cur = item.end;
1905
2016
  }
1906
2017
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1907
2018
  }
1908
2019
  };
1909
2020
  }
1910
- function oneOrMore(combinator) {
2021
+ function oneOrMore(combinator, opts = {}) {
2022
+ return many(combinator, { ...opts, min: opts.min ?? 1 });
2023
+ }
2024
+ function atLeast(combinator, min, max) {
1911
2025
  const meta = {
1912
2026
  firstSet: combinator._meta.firstSet,
1913
2027
  canMatchNewline: combinator._meta.canMatchNewline,
1914
2028
  isTrivia: false
1915
2029
  };
1916
- const def = { tag: "oneOrMore", parser: combinator, min: 1 };
2030
+ const def = { tag: "oneOrMore", parser: combinator, min, ...max === Infinity ? {} : { max } };
1917
2031
  let expected;
1918
2032
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1919
2033
  return {
@@ -1925,7 +2039,19 @@ function oneOrMore(combinator) {
1925
2039
  if (!first.ok) return first;
1926
2040
  const values = def.valueUnused ? void 0 : [first.value];
1927
2041
  let cur = first.span.end;
2042
+ let count = 1;
2043
+ while (count < min) {
2044
+ const item = repItem(combinator, input, cur, ctx, guardable);
2045
+ if (item === "stop" || "fail" in item) {
2046
+ expected ??= deriveExpected(combinator);
2047
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
2048
+ }
2049
+ if (values !== void 0) values.push(item.value);
2050
+ cur = item.end;
2051
+ count++;
2052
+ }
1928
2053
  while (cur < input.length) {
2054
+ if (count >= max) break;
1929
2055
  const item = repItem(combinator, input, cur, ctx, guardable);
1930
2056
  if (item === "stop") break;
1931
2057
  if ("fail" in item) {
@@ -1936,10 +2062,12 @@ function oneOrMore(combinator) {
1936
2062
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1937
2063
  if (values !== void 0) values.push(error);
1938
2064
  captureError(ctx, error);
2065
+ count++;
1939
2066
  cur = end;
1940
2067
  continue;
1941
2068
  }
1942
2069
  if (values !== void 0) values.push(item.value);
2070
+ count++;
1943
2071
  cur = item.end;
1944
2072
  }
1945
2073
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
@@ -1969,17 +2097,33 @@ function optional(combinator) {
1969
2097
  }
1970
2098
  };
1971
2099
  }
1972
- function sepBy(combinator, separator) {
2100
+ function oneOrMoreSep(combinator, separator, opts = {}) {
2101
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 });
2102
+ }
2103
+ function sepBy(combinator, separator, opts = {}) {
2104
+ const { min, max } = resolveBounds("sepBy()", opts);
2105
+ const trailing = opts.trailing ?? "forbid";
1973
2106
  const meta = {
1974
2107
  firstSet: combinator._meta.firstSet,
1975
2108
  canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
1976
2109
  isTrivia: false
1977
2110
  };
1978
2111
  let expected;
2112
+ const failAt2 = (at) => {
2113
+ expected ??= deriveExpected(combinator);
2114
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } };
2115
+ };
1979
2116
  return {
1980
2117
  _tag: "sepBy",
1981
2118
  _meta: meta,
1982
- _def: { tag: "sepBy", parser: combinator, separator },
2119
+ _def: {
2120
+ tag: "sepBy",
2121
+ parser: combinator,
2122
+ separator,
2123
+ min,
2124
+ ...max === Infinity ? {} : { max },
2125
+ ...trailing === "forbid" ? {} : { trailing }
2126
+ },
1983
2127
  parse(input, pos, ctx) {
1984
2128
  const first = combinator.parse(input, pos, ctx);
1985
2129
  const values = [];
@@ -1990,6 +2134,7 @@ function sepBy(combinator, separator) {
1990
2134
  } else {
1991
2135
  const term = ctx._tolerant ? ctx._sync : void 0;
1992
2136
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2137
+ if (min >= 1) return failAt2(pos);
1993
2138
  return { ok: true, value: [], span: { start: pos, end: pos } };
1994
2139
  }
1995
2140
  expected ??= deriveExpected(combinator);
@@ -1999,6 +2144,7 @@ function sepBy(combinator, separator) {
1999
2144
  cur = rec.end;
2000
2145
  }
2001
2146
  while (cur < input.length) {
2147
+ if (values.length >= max) break;
2002
2148
  const loopMark = saveTriviaMark(ctx);
2003
2149
  let sepPos = cur;
2004
2150
  if (ctx.trivia) {
@@ -2015,6 +2161,7 @@ function sepBy(combinator, separator) {
2015
2161
  rollbackTrivia(ctx, loopMark);
2016
2162
  break;
2017
2163
  }
2164
+ const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
2018
2165
  let nextPos = sep.span.end;
2019
2166
  if (ctx.trivia) {
2020
2167
  if (needsDeferredTriviaCommit(ctx)) {
@@ -2036,12 +2183,18 @@ function sepBy(combinator, separator) {
2036
2183
  cur = rec.end;
2037
2184
  continue;
2038
2185
  }
2186
+ if (sepMark !== void 0) {
2187
+ rollbackTrivia(ctx, sepMark);
2188
+ cur = sep.span.end;
2189
+ break;
2190
+ }
2039
2191
  rollbackTrivia(ctx, loopMark);
2040
2192
  break;
2041
2193
  }
2042
2194
  values.push(next.value);
2043
2195
  cur = next.span.end;
2044
2196
  }
2197
+ if (values.length < min) return failAt2(cur);
2045
2198
  return { ok: true, value: values, span: { start: pos, end: cur } };
2046
2199
  }
2047
2200
  };
@@ -2131,6 +2284,7 @@ function markUnusedValues(root) {
2131
2284
  visit(def.separator, false);
2132
2285
  return;
2133
2286
  case "not":
2287
+ case "peek":
2134
2288
  visit(def.parser, false);
2135
2289
  return;
2136
2290
  case "grammar":
@@ -2249,9 +2403,9 @@ function not(combinator) {
2249
2403
  _meta: meta,
2250
2404
  _def: { tag: "not", parser: combinator },
2251
2405
  parse(input, pos, ctx) {
2252
- const mark = saveCstMark(ctx);
2406
+ const mark = saveLookaheadMark(ctx);
2253
2407
  const result = combinator.parse(input, pos, ctx);
2254
- rollbackCstCapture(ctx, mark);
2408
+ rollbackLookahead(ctx, mark);
2255
2409
  if (result.ok) {
2256
2410
  return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } };
2257
2411
  }
@@ -2260,6 +2414,28 @@ function not(combinator) {
2260
2414
  };
2261
2415
  }
2262
2416
 
2417
+ // src/combinators/peek.ts
2418
+ function peek(combinator) {
2419
+ const meta = {
2420
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
2421
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
2422
+ canMatchNewline: false,
2423
+ isTrivia: false
2424
+ };
2425
+ return {
2426
+ _tag: "peek",
2427
+ _meta: meta,
2428
+ _def: { tag: "peek", parser: combinator },
2429
+ parse(input, pos, ctx) {
2430
+ const mark = saveLookaheadMark(ctx);
2431
+ const result = combinator.parse(input, pos, ctx);
2432
+ rollbackLookahead(ctx, mark);
2433
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } };
2434
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } };
2435
+ }
2436
+ };
2437
+ }
2438
+
2263
2439
  // src/compiler/build-arity.ts
2264
2440
  var PARAM_LIST_RE = (
2265
2441
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
@@ -2358,6 +2534,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2358
2534
  case "expect":
2359
2535
  case "withCtx":
2360
2536
  case "not":
2537
+ case "peek":
2361
2538
  return parserHasOwnFields(d.parser, seen);
2362
2539
  default:
2363
2540
  return false;
@@ -2397,6 +2574,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2397
2574
  case "expect":
2398
2575
  case "withCtx":
2399
2576
  case "not":
2577
+ case "peek":
2400
2578
  case "field":
2401
2579
  return parserHasTriviaSite(d.parser, seen);
2402
2580
  case "choice":
@@ -2446,6 +2624,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2446
2624
  case "expect":
2447
2625
  case "withCtx":
2448
2626
  case "not":
2627
+ case "peek":
2449
2628
  return parserEnablesTriviaCapture(d.parser, seen);
2450
2629
  // leaf() clears the inner CST/trivia collector and publishes only its own
2451
2630
  // terminal result. An inner parser() capture cannot reach this node.
@@ -2821,7 +3000,7 @@ function leaf(root, fn) {
2821
3000
  }
2822
3001
 
2823
3002
  // src/version.ts
2824
- var PARSEMAN_VERSION = "0.32.0";
3003
+ var PARSEMAN_VERSION = "0.34.0";
2825
3004
 
2826
3005
  // src/analysis/gating.ts
2827
3006
  var isAny = (fs) => fs.kind === "any";
@@ -2861,14 +3040,15 @@ function firstSetToString(fs) {
2861
3040
  var ruleNameOf = (p) => p._ruleName;
2862
3041
  var SUGGESTIONS = {
2863
3042
  "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.",
3043
+ "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 \u2014 use peek(X), which is zero-width AND carries X first-set.",
3044
+ "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. A plain sepBy(item, sep) is ALSO nullable \u2014 pass { min: 1 } for a list that cannot be empty.",
2866
3045
  "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
3046
  "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2868
3047
  "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2869
3048
  };
2870
- function classifyBroadArm(arm) {
3049
+ function classifyBroadArm(arm, resolve) {
2871
3050
  const seen = /* @__PURE__ */ new Set();
3051
+ const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
2872
3052
  const walk2 = (p) => {
2873
3053
  if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2874
3054
  seen.add(p);
@@ -2880,6 +3060,10 @@ function classifyBroadArm(arm) {
2880
3060
  return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2881
3061
  case "not":
2882
3062
  return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
3063
+ // peek() CARRIES its body's first-set, so it is only broad when the body is
3064
+ // (or the body is nullable) — walk in for the real cause.
3065
+ case "peek":
3066
+ return walk2(d.parser);
2883
3067
  case "scanTo":
2884
3068
  return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2885
3069
  // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
@@ -2896,18 +3080,23 @@ function classifyBroadArm(arm) {
2896
3080
  return walk2(d.parser);
2897
3081
  case "lazy": {
2898
3082
  const name = p._ruleName;
3083
+ let target;
2899
3084
  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;
3085
+ target = d.thunk();
2903
3086
  } catch {
2904
- return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3087
+ const bound = name !== void 0 ? resolve?.(name) : void 0;
3088
+ if (bound === void 0)
3089
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}`, unresolvedExternal: name !== void 0 };
3090
+ target = bound;
2905
3091
  }
3092
+ const inner = walk2(target);
3093
+ if (name !== void 0) return inner ? relabel(inner, "cross-artifact-ref", `via ref g.${name} \u2192 ${inner.detail}`) : null;
3094
+ return inner;
2906
3095
  }
2907
3096
  case "optional":
2908
3097
  case "many": {
2909
3098
  const inner = walk2(d.parser);
2910
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3099
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2911
3100
  }
2912
3101
  case "oneOrMore":
2913
3102
  case "transform":
@@ -2929,13 +3118,13 @@ function classifyBroadArm(arm) {
2929
3118
  sawNullablePrefix = true;
2930
3119
  continue;
2931
3120
  }
2932
- if (isAny(firstSetOf(t))) {
3121
+ if (isAny(firstSetOf(t, /* @__PURE__ */ new Set(), resolve))) {
2933
3122
  const inner = walk2(t);
2934
3123
  if (sawNullablePrefix)
2935
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3124
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2936
3125
  return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2937
3126
  }
2938
- if (matchesEmpty(t)) {
3127
+ if (matchesEmpty(t, /* @__PURE__ */ new Set(), resolve)) {
2939
3128
  sawNullablePrefix = true;
2940
3129
  continue;
2941
3130
  }
@@ -2946,7 +3135,7 @@ function classifyBroadArm(arm) {
2946
3135
  case "choice": {
2947
3136
  for (const a of d.parsers) {
2948
3137
  const r = walk2(a);
2949
- if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3138
+ if (r) return relabel(r, r.cause, `choice arm \u2192 ${r.detail}`);
2950
3139
  }
2951
3140
  return null;
2952
3141
  }
@@ -2983,7 +3172,7 @@ function detectAntiPatterns(rule, arms) {
2983
3172
  kind: "double-not",
2984
3173
  rule,
2985
3174
  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."
3175
+ message: "not(not(...)) hand-rolls automatic first-char gating and MISCOMPILES among shared-first-char sibling arms (its first-set is ANY, poisoning dispatch). Prefer letting the arm lead with its consuming terminal; where a real positive lookahead IS needed, use peek(X) \u2014 zero-width like not(not(X)) but it CARRIES X's first-set, so the arm still dispatches."
2987
3176
  });
2988
3177
  } else {
2989
3178
  out.push({
@@ -3006,6 +3195,9 @@ function detectAntiPatterns(rule, arms) {
3006
3195
  return out;
3007
3196
  }
3008
3197
  function analyzeGating(entry, opts) {
3198
+ return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3199
+ }
3200
+ function analyzeGatingRules(ruleMap, opts) {
3009
3201
  const raw = [];
3010
3202
  const antiPatterns = [];
3011
3203
  const visited = /* @__PURE__ */ new Set();
@@ -3015,7 +3207,7 @@ function analyzeGating(entry, opts) {
3015
3207
  const d = p._def;
3016
3208
  const rule = ruleNameOf(p) ?? enclosingRule;
3017
3209
  if (d.tag === "choice") {
3018
- raw.push({ g: analyzeChoice(p, d, rule), rule });
3210
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
3019
3211
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3020
3212
  }
3021
3213
  const rec = d;
@@ -3032,7 +3224,7 @@ function analyzeGating(entry, opts) {
3032
3224
  }
3033
3225
  for (const k of kids) visit(k, rule);
3034
3226
  };
3035
- visit(entry, ruleNameOf(entry) ?? "<entry>");
3227
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3036
3228
  const perRule = /* @__PURE__ */ new Map();
3037
3229
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3038
3230
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3048,18 +3240,19 @@ function analyzeGating(entry, opts) {
3048
3240
  });
3049
3241
  const gated = choices.filter((c) => c.gates === "yes").length;
3050
3242
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3051
- const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3243
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted && !c.deferred);
3052
3244
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3245
+ const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3053
3246
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3054
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3247
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3055
3248
  }
3056
- function analyzeChoice(p, d, rule) {
3249
+ function analyzeChoice(p, d, rule, resolve) {
3057
3250
  const arms = d.parsers;
3058
3251
  const shallow = arms.map((a) => a._meta.firstSet);
3059
- const deep = arms.map((a) => firstSetOf(a));
3252
+ const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
3060
3253
  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] };
3254
+ const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
3255
+ return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
3063
3256
  });
3064
3257
  const overlaps = [];
3065
3258
  for (let i = 0; i < deep.length; i++)
@@ -3075,10 +3268,12 @@ function analyzeChoice(p, d, rule) {
3075
3268
  if (d.disjoint) gates = "yes";
3076
3269
  else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3077
3270
  else gates = "no";
3271
+ const deferred = gates === "no" && overlaps.length === 0 && anyArms.length > 0 && anyArms.every((a) => a.unresolvedExternal);
3078
3272
  return {
3079
3273
  rule,
3080
3274
  strategy: d.strategy?.tag ?? "firstMatch",
3081
3275
  gates,
3276
+ deferred,
3082
3277
  combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3083
3278
  anyArms,
3084
3279
  overlaps
@@ -3245,6 +3440,7 @@ function children(def, winners) {
3245
3440
  case "field":
3246
3441
  case "grammar":
3247
3442
  case "not":
3443
+ case "peek":
3248
3444
  case "node":
3249
3445
  case "guard":
3250
3446
  case "withCtx":
@@ -5010,6 +5206,9 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5010
5206
  case "keywords":
5011
5207
  case "guard":
5012
5208
  case "not":
5209
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
5210
+ // outcomes, so it can never leave a partial capture behind.
5211
+ case "peek":
5013
5212
  case "trivia":
5014
5213
  case "token":
5015
5214
  case "leaf":
@@ -5081,6 +5280,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
5081
5280
  case "leaf":
5082
5281
  return true;
5083
5282
  case "not":
5283
+ case "peek":
5084
5284
  case "guard":
5085
5285
  case "trivia":
5086
5286
  case "unknown":
@@ -5207,6 +5407,7 @@ function emitKeywordsFast(def, ctx, pos) {
5207
5407
  if (def.words.length === 0 || def.words.some((w) => w.length === 0)) return null;
5208
5408
  if (def.words.some((w) => Array.from(w).length !== w.length)) return null;
5209
5409
  if (def.caseInsensitive && def.boundary) return null;
5410
+ if (def.caseInsensitive && def.words.some((w) => w.split("").some((c) => c.charCodeAt(0) > 127))) return null;
5210
5411
  let boundary = null;
5211
5412
  if (def.boundary) {
5212
5413
  let body = def.boundary;
@@ -5254,7 +5455,7 @@ function emitKeywords(def, ctx, pos) {
5254
5455
  if (fast) return fast;
5255
5456
  const alt = def.words.map(escapeKeywordRe).join("|");
5256
5457
  const boundary = def.boundary ? `(?![${def.boundary}])` : "";
5257
- const flags = def.caseInsensitive ? "iuy" : "uy";
5458
+ const flags = def.caseInsensitive ? "iy" : "uy";
5258
5459
  const source = `(?:${alt})${boundary}`;
5259
5460
  const key = `${source}/${flags}`;
5260
5461
  let rName = ctx.regexMap.get(key);
@@ -5400,6 +5601,7 @@ function failsAtStart(p) {
5400
5601
  case "keywords":
5401
5602
  case "guard":
5402
5603
  case "not":
5604
+ case "peek":
5403
5605
  return true;
5404
5606
  case "transform":
5405
5607
  case "label":
@@ -5813,14 +6015,17 @@ function emitMany(def, ctx, pos) {
5813
6015
  ];
5814
6016
  const mySyncV = ctx.recovery ? v(ctx, "_mysy") : "";
5815
6017
  if (ctx.recovery) stmts.push(`${ind(ctx)}const ${mySyncV} = _ctx._sync`);
5816
- if (def.min === 1) {
6018
+ for (let i = 0; i < def.min; i++) {
5817
6019
  const firstR = emit(def.parser, ctx, curV);
5818
6020
  stmts.push(...firstR.stmts);
5819
6021
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
5820
6022
  stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
5821
6023
  }
6024
+ const maxV = def.max === void 0 ? null : wantValue ? `${arrV}.length` : v(ctx, "_cnt");
6025
+ if (maxV !== null && !wantValue) stmts.push(`${ind(ctx)}let ${maxV} = ${def.min}`);
5822
6026
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
5823
6027
  ctx.indent++;
6028
+ if (maxV !== null) stmts.push(`${ind(ctx)}if (${maxV} >= ${def.max}) break`);
5824
6029
  let itemPos = curV;
5825
6030
  let rollback = "";
5826
6031
  if (ctx.activeTrivia) {
@@ -5861,7 +6066,7 @@ function emitMany(def, ctx, pos) {
5861
6066
  `${ind(ctx)}if (!${iterOk}) {`,
5862
6067
  `${ind(ctx)} ${rollback}if (_ctx._tolerant && ${mySyncV} !== undefined && !_ctx._rec.at(${mySyncV}, input, ${itemPos}, _ctx)) {`,
5863
6068
  `${ind(ctx)} const ${rrV} = _ctx._rec.scan(input, ${itemPos}, _ctx, ${mySyncV}, ${exp})`,
5864
- ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : [],
6069
+ ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : maxV !== null ? [`${ind(ctx)} ${maxV}++`] : [],
5865
6070
  `${ind(ctx)} _ctx._rec.capture(_ctx, ${rrV}.error)`,
5866
6071
  `${ind(ctx)} ${curV} = ${rrV}.end`,
5867
6072
  `${ind(ctx)} continue`,
@@ -5874,6 +6079,7 @@ function emitMany(def, ctx, pos) {
5874
6079
  stmts.push(`${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`);
5875
6080
  }
5876
6081
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
6082
+ else if (maxV !== null) stmts.push(`${ind(ctx)}${maxV}++`);
5877
6083
  stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
5878
6084
  ctx.indent--;
5879
6085
  stmts.push(`${ind(ctx)}}`);
@@ -5938,8 +6144,8 @@ function emitSepBy(_p, def, ctx, pos) {
5938
6144
  ...rec ? [`${ind(ctx)}const ${mySyncV} = _ctx._sync`] : [],
5939
6145
  ...firstStmts
5940
6146
  ];
5941
- const failItem = (nextOkVar, itemPosVar, rb, originalFail) => {
5942
- if (!rec) return [originalFail];
6147
+ const failItem = (nextOkVar, itemPosVar, breakStmt) => {
6148
+ if (!rec) return [`${ind(ctx)}if (!${nextOkVar}) ${breakStmt === "break" ? "break" : `{ ${breakStmt} }`}`];
5943
6149
  const rr = v(ctx, "_rr");
5944
6150
  return [
5945
6151
  `${ind(ctx)}if (!${nextOkVar}) {`,
@@ -5947,10 +6153,24 @@ function emitSepBy(_p, def, ctx, pos) {
5947
6153
  `${ind(ctx)} const ${rr} = _ctx._rec.scan(input, ${itemPosVar}, _ctx, ${scanSync}, ${exp})`,
5948
6154
  `${ind(ctx)} ${arrV}.push(${rr}.error); _ctx._rec.capture(_ctx, ${rr}.error); ${curV} = ${rr}.end; continue`,
5949
6155
  `${ind(ctx)} }`,
5950
- `${ind(ctx)} ${rb}break`,
6156
+ `${ind(ctx)} ${breakStmt}`,
5951
6157
  `${ind(ctx)}}`
5952
6158
  ];
5953
6159
  };
6160
+ const itemFailBreak = (sepEndVar, sepRb, postSepRb) => def.trailing === void 0 ? `${sepRb}break` : `${postSepRb}${curV} = ${sepEndVar}; break`;
6161
+ const postSepMarks = () => {
6162
+ if (def.trailing === void 0 || !ctx.capturing) return { decl: [], rb: "" };
6163
+ const lv = v(ctx, "_tlv"), rw = v(ctx, "_trw"), tl = v(ctx, "_ttl"), lg = v(ctx, "_tlg");
6164
+ return {
6165
+ decl: [
6166
+ `${ind(ctx)}const ${lv} = _ctx._cstLeaves ? _ctx._cstLeaves.length : 0`,
6167
+ `${ind(ctx)}const ${rw} = _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0`,
6168
+ `${ind(ctx)}const ${tl} = _ctx._cstTriviaLog ? _ctx._cstTriviaLog.length : 0`,
6169
+ `${ind(ctx)}const ${lg} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`
6170
+ ],
6171
+ rb: `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${lv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${rw}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${tl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${lg}; `
6172
+ };
6173
+ };
5954
6174
  if (rec) {
5955
6175
  const rr0 = v(ctx, "_rr0");
5956
6176
  const didV = v(ctx, "_did");
@@ -5976,6 +6196,7 @@ function emitSepBy(_p, def, ctx, pos) {
5976
6196
  );
5977
6197
  ctx.indent++;
5978
6198
  }
6199
+ if (def.max !== void 0) stmts.push(`${ind(ctx)}if (${arrV}.length >= ${def.max}) break`);
5979
6200
  let sepAtPos = curV;
5980
6201
  if (ctx.activeTrivia) {
5981
6202
  if (ctx.capturing) {
@@ -5996,12 +6217,14 @@ function emitSepBy(_p, def, ctx, pos) {
5996
6217
  const rollbackToSep = `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${markLog}; `;
5997
6218
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
5998
6219
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) { ${rollbackToSep}break }`);
6220
+ const post = postSepMarks();
6221
+ stmts.push(...post.decl);
5999
6222
  const npV = v(ctx, "_np");
6000
6223
  stmts.push(`${ind(ctx)}const ${npV} = ${capFn}(input, ${sepEnd}, _ctx, 1)`);
6001
6224
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6002
6225
  stmts.push(
6003
6226
  ...nextStmts,
6004
- ...failItem(nextOk, npV, rollbackToSep, `${ind(ctx)}if (!${nextOk}) { ${rollbackToSep}break }`),
6227
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, rollbackToSep, post.rb)),
6005
6228
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6006
6229
  `${ind(ctx)}${curV} = ${nextEnd}`
6007
6230
  );
@@ -6017,7 +6240,7 @@ function emitSepBy(_p, def, ctx, pos) {
6017
6240
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6018
6241
  stmts.push(
6019
6242
  ...nextStmts,
6020
- ...failItem(nextOk, npV, "", `${ind(ctx)}if (!${nextOk}) break`),
6243
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, "", "")),
6021
6244
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6022
6245
  `${ind(ctx)}${curV} = ${nextEnd}`
6023
6246
  );
@@ -6034,10 +6257,12 @@ function emitSepBy(_p, def, ctx, pos) {
6034
6257
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6035
6258
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) break`);
6036
6259
  const nextRb = markLv ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6260
+ const post = postSepMarks();
6261
+ stmts.push(...post.decl);
6037
6262
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, sepEnd, true);
6038
6263
  stmts.push(
6039
6264
  ...nextStmts,
6040
- ...failItem(nextOk, sepEnd, nextRb, `${ind(ctx)}if (!${nextOk}) { ${nextRb}break }`),
6265
+ ...failItem(nextOk, sepEnd, itemFailBreak(sepEnd, nextRb, post.rb)),
6041
6266
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6042
6267
  `${ind(ctx)}${curV} = ${nextEnd}`
6043
6268
  );
@@ -6046,6 +6271,9 @@ function emitSepBy(_p, def, ctx, pos) {
6046
6271
  stmts.push(`${ind(ctx)}}`);
6047
6272
  ctx.indent--;
6048
6273
  stmts.push(`${ind(ctx)}}`);
6274
+ if (def.min >= 1) {
6275
+ stmts.push(...emitIfFail(ctx, `${arrV}.length < ${def.min}`, failArrBody(ctx, deriveExpectedArr([def.parser]), curV)));
6276
+ }
6049
6277
  return { stmts, valueVar: arrV, endVar: curV };
6050
6278
  }
6051
6279
  function emitScanTo(def, ctx, pos) {
@@ -6097,17 +6325,44 @@ function emitScanTo(def, ctx, pos) {
6097
6325
  return { stmts, valueVar: valV, endVar: curV };
6098
6326
  }
6099
6327
  function emitNot(def, ctx, pos) {
6328
+ const sinksLive = !!ctx.capturing || !!ctx.recovery;
6329
+ const leaves = sinksLive ? v(ctx, "_ntl") : null;
6330
+ const raw = sinksLive ? v(ctx, "_ntr") : null;
6331
+ const tl = sinksLive ? v(ctx, "_ntt") : null;
6332
+ const log = sinksLive ? v(ctx, "_ntg") : null;
6333
+ const fields = sinksLive ? v(ctx, "_ntf") : null;
6334
+ const errors = sinksLive ? v(ctx, "_nte") : null;
6100
6335
  const { stmts, okVar } = emitFallible(def.parser, ctx, pos, true);
6101
6336
  const label2 = JSON.stringify(`not(${def.parser._tag})`);
6102
6337
  return {
6103
6338
  stmts: [
6339
+ ...sinksLive ? [
6340
+ `${ind(ctx)}const ${leaves} = _ctx._cstLeaves?.length ?? 0, ${raw} = _ctx._cstRawChildren?.length ?? 0, ${tl} = _ctx._cstTriviaLog?.length ?? 0, ${log} = _ctx._triviaLog?.length ?? 0, ${fields} = _ctx._fields?.length ?? 0, ${errors} = _ctx._errors?.length ?? 0`
6341
+ ] : [],
6104
6342
  ...stmts,
6343
+ ...sinksLive ? [
6344
+ `${ind(ctx)}if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${leaves}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${raw}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${tl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${log}; if (_ctx._fields) _ctx._fields.length = ${fields}; if (_ctx._errors) _ctx._errors.length = ${errors}`
6345
+ ] : [],
6105
6346
  ...emitIfFail(ctx, okVar, failBody(ctx, label2, pos))
6106
6347
  ],
6107
6348
  valueVar: "null",
6108
6349
  endVar: pos
6109
6350
  };
6110
6351
  }
6352
+ function emitPeek(def, ctx, pos) {
6353
+ const probeCtx = { ...ctx, capturing: false, noHoist: true };
6354
+ const { stmts, okVar } = emitFallible(def.parser, probeCtx, pos, true);
6355
+ ctx.vars = probeCtx.vars;
6356
+ const label2 = JSON.stringify(`peek(${def.parser._tag})`);
6357
+ return {
6358
+ stmts: [
6359
+ ...stmts,
6360
+ ...emitIfFail(ctx, `!${okVar}`, failBody(ctx, label2, pos))
6361
+ ],
6362
+ valueVar: "null",
6363
+ endVar: pos
6364
+ };
6365
+ }
6111
6366
  function escapeTokenLiteral(value) {
6112
6367
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6113
6368
  }
@@ -6395,9 +6650,9 @@ function emitRuntimeFallback(parser2, ctx, pos) {
6395
6650
  return { stmts, valueVar: vv, endVar: ev };
6396
6651
  }
6397
6652
  function emitLazy(p, def, ctx, pos) {
6398
- const canonical = ctx.ruleNames?.get(p);
6653
+ const canonical2 = ctx.ruleNames?.get(p);
6399
6654
  const usage = ctx.lazyUsage;
6400
- if (!canonical && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6655
+ if (!canonical2 && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6401
6656
  let resolved;
6402
6657
  try {
6403
6658
  resolved = def.thunk();
@@ -6407,7 +6662,7 @@ function emitLazy(p, def, ctx, pos) {
6407
6662
  return emit(resolved, ctx, pos);
6408
6663
  }
6409
6664
  if (!ctx.namedParsers.has(p)) {
6410
- const fnName2 = canonical ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6665
+ const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6411
6666
  ctx.namedParsers.set(p, fnName2);
6412
6667
  let resolved;
6413
6668
  try {
@@ -6690,6 +6945,8 @@ function emitDispatch(p, ctx, pos) {
6690
6945
  }
6691
6946
  case "not":
6692
6947
  return emitNot(def, ctx, pos);
6948
+ case "peek":
6949
+ return emitPeek(def, ctx, pos);
6693
6950
  case "node":
6694
6951
  return emitNode(def, ctx, pos);
6695
6952
  case "scanTo":
@@ -6778,6 +7035,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
6778
7035
  case "many":
6779
7036
  case "oneOrMore":
6780
7037
  case "not":
7038
+ case "peek":
6781
7039
  case "transform":
6782
7040
  return hasNodeDef(d.parser, seen);
6783
7041
  case "skip":
@@ -6829,17 +7087,18 @@ function childrenOf(def) {
6829
7087
  case "field":
6830
7088
  case "grammar":
6831
7089
  case "not":
7090
+ case "peek":
6832
7091
  case "node":
6833
7092
  case "withCtx":
6834
7093
  case "expect":
6835
7094
  return [def.parser];
6836
- // emitMany's oneOrMore branch and emitSepBy both codegen `def.parser` TWICE
6837
- // (a mandatory first match, then again inside the repeat loop) — two real
6838
- // emit() call sites, so the usage analysis must see two edges here or it
7095
+ // emitMany's min>=1 branch and emitSepBy both codegen `def.parser` more than
7096
+ // once (`min` mandatory matches, then again inside the repeat loop) — each is a
7097
+ // real emit() call site, so the usage analysis must see one edge per site or it
6839
7098
  // undercounts a single-use `parser` ref as inline-safe when it's actually
6840
- // referenced from two positions within this one compiled function.
7099
+ // referenced from several positions within this one compiled function.
6841
7100
  case "oneOrMore":
6842
- return [def.parser, def.parser];
7101
+ return Array.from({ length: def.min + 1 }, () => def.parser);
6843
7102
  case "sepBy":
6844
7103
  return [def.parser, def.parser, def.separator];
6845
7104
  case "skip":
@@ -6985,12 +7244,41 @@ function resolveGatingLevel(opt) {
6985
7244
  return "warn";
6986
7245
  }
6987
7246
  function runGatingDiagnostic(combinator, opt) {
7247
+ return reportGating(opt, (o) => analyzeGating(combinator, o));
7248
+ }
7249
+ function runGatingDiagnosticRules(ruleMap, opt) {
7250
+ return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7251
+ }
7252
+ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7253
+ if (resolveGatingLevel(opt) === "off") return void 0;
7254
+ const ruleMaps = getRuleMaps();
7255
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
7256
+ const winners = /* @__PURE__ */ new Map();
7257
+ for (const map of ruleMaps) for (const [name, rule] of map) {
7258
+ if (rule._def.tag === "lazy") {
7259
+ try {
7260
+ rule._def.thunk();
7261
+ } catch {
7262
+ continue;
7263
+ }
7264
+ }
7265
+ winners.set(name, rule);
7266
+ }
7267
+ const entries = [...winners];
7268
+ return reportGating(opt, (o) => {
7269
+ const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7270
+ const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7271
+ return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
7272
+ });
7273
+ }
7274
+ function reportGating(opt, analyze) {
6988
7275
  const level = resolveGatingLevel(opt);
6989
7276
  if (level === "off") return void 0;
6990
- const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7277
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
7278
+ const analyzeOpts = obj?.accept !== void 0 || obj?.entryName !== void 0 ? { ...obj.accept !== void 0 ? { accept: obj.accept } : {}, ...obj.entryName !== void 0 ? { entryName: obj.entryName } : {} } : void 0;
6991
7279
  let report;
6992
7280
  try {
6993
- report = analyzeGating(combinator, accept ? { accept } : void 0);
7281
+ report = analyze(analyzeOpts);
6994
7282
  } catch {
6995
7283
  return void 0;
6996
7284
  }
@@ -7127,6 +7415,7 @@ function publicRuleWrapperSource(rule, fnSource) {
7127
7415
  }
7128
7416
  function compileLinkable(ruleMapArg, ns, opts) {
7129
7417
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7418
+ if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
7130
7419
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7131
7420
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7132
7421
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
@@ -7183,6 +7472,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7183
7472
  fnNameToKey.set(fn, key);
7184
7473
  }
7185
7474
  ctx.ruleNames = ruleNames;
7475
+ const externalRefs = /* @__PURE__ */ new Set();
7186
7476
  {
7187
7477
  const scanned = /* @__PURE__ */ new Set();
7188
7478
  const scanExternal = (p) => {
@@ -7198,11 +7488,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
7198
7488
  }
7199
7489
  if (resolved === void 0) {
7200
7490
  const name = p._ruleName;
7201
- if (name && !ctx.namedParsers.has(p)) {
7202
- assertRuleName(name);
7203
- const fn = `_r_${name}`;
7204
- ruleNames.set(p, fn);
7205
- ctx.namedParsers.set(p, fn);
7491
+ if (name) {
7492
+ externalRefs.add(p);
7493
+ if (!ctx.namedParsers.has(p)) {
7494
+ assertRuleName(name);
7495
+ const fn = `_r_${name}`;
7496
+ ruleNames.set(p, fn);
7497
+ ctx.namedParsers.set(p, fn);
7498
+ }
7206
7499
  }
7207
7500
  return;
7208
7501
  }
@@ -7315,12 +7608,32 @@ function compileLinkable(ruleMapArg, ns, opts) {
7315
7608
  needsEmptyTl: !!ctx.needsEmptyTl,
7316
7609
  needsHostReads: !!ctx.needsHostReads,
7317
7610
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7318
- isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule)),
7611
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7319
7612
  mfFns: mfSrcs ? [] : ctx.mapFns,
7320
7613
  buildFns: buildSrcs ? [] : ctx.buildFns
7321
7614
  };
7322
7615
  }
7323
- function hasSemanticReduction(roots) {
7616
+ function hasExternalRuleRef(ruleMap) {
7617
+ const seen = /* @__PURE__ */ new Set();
7618
+ const visit = (p) => {
7619
+ if (seen.has(p)) return false;
7620
+ seen.add(p);
7621
+ const def = p._def;
7622
+ if (def.tag === "lazy") {
7623
+ let resolved;
7624
+ try {
7625
+ resolved = def.thunk();
7626
+ } catch {
7627
+ resolved = void 0;
7628
+ }
7629
+ if (resolved === void 0) return p._ruleName !== void 0;
7630
+ return visit(resolved);
7631
+ }
7632
+ return childrenOf(def).some(visit);
7633
+ };
7634
+ return ruleMap.some(([, rule]) => visit(rule));
7635
+ }
7636
+ function hasSemanticReduction(roots, externalRefs) {
7324
7637
  const seen = /* @__PURE__ */ new Set();
7325
7638
  const visit = (parser2) => {
7326
7639
  if (seen.has(parser2)) return false;
@@ -7331,6 +7644,7 @@ function hasSemanticReduction(roots) {
7331
7644
  if (def.tag === "guard" || def.tag === "withCtx") return true;
7332
7645
  if (def.tag === "node" && def.build !== void 0) return true;
7333
7646
  if (def.tag === "lazy") {
7647
+ if (externalRefs?.has(parser2)) return false;
7334
7648
  try {
7335
7649
  return visit(def.thunk());
7336
7650
  } catch {
@@ -7422,6 +7736,7 @@ function childrenOf2(def) {
7422
7736
  case "label":
7423
7737
  case "field":
7424
7738
  case "not":
7739
+ case "peek":
7425
7740
  case "node":
7426
7741
  case "withCtx":
7427
7742
  case "expect":
@@ -7455,9 +7770,16 @@ function lazyTarget(c) {
7455
7770
  }
7456
7771
  var Unserializable = class extends Error {
7457
7772
  };
7458
- function serializeRuleMap(ruleMap) {
7773
+ function repeatOpts(min, max, trailing) {
7774
+ const parts = [];
7775
+ if (min !== 0) parts.push(`min: ${min}`);
7776
+ if (max !== void 0) parts.push(`max: ${max}`);
7777
+ if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
7778
+ return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
7779
+ }
7780
+ function serializeRuleMap(ruleMap, scanSkip) {
7459
7781
  try {
7460
- return new Serializer(ruleMap).run();
7782
+ return new Serializer(ruleMap, scanSkip ?? []).run();
7461
7783
  } catch (e) {
7462
7784
  if (e instanceof Unserializable) {
7463
7785
  if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${e.message}`);
@@ -7558,6 +7880,7 @@ function evalRuleMapIR(ir) {
7558
7880
  "optional",
7559
7881
  "sepBy",
7560
7882
  "not",
7883
+ "peek",
7561
7884
  "node",
7562
7885
  "parser",
7563
7886
  "scanTo",
@@ -7590,6 +7913,7 @@ function evalRuleMapIR(ir) {
7590
7913
  optional,
7591
7914
  sepBy,
7592
7915
  not,
7916
+ peek,
7593
7917
  node,
7594
7918
  parser,
7595
7919
  scanTo,
@@ -7618,10 +7942,28 @@ var Serializer = class {
7618
7942
  decls = [];
7619
7943
  ruleValues = /* @__PURE__ */ new Set();
7620
7944
  ruleMap;
7621
- constructor(ruleMap) {
7945
+ scanSkip;
7946
+ constructor(ruleMap, scanSkip = []) {
7622
7947
  this.ruleMap = ruleMap;
7948
+ this.scanSkip = scanSkip;
7623
7949
  for (const [, c] of ruleMap) this.ruleValues.add(this.body(c));
7624
7950
  }
7951
+ /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
7952
+ * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
7953
+ * serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
7954
+ * outside the `(g) => …` factory where `g` is bound. A unit that does reference a
7955
+ * rule can't be carried in options; bail to the full-pieces fallback. */
7956
+ assertNoRuleRef(c, seen) {
7957
+ if (seen.has(c)) return;
7958
+ seen.add(c);
7959
+ if (c._def.tag === "lazy") {
7960
+ if (ruleNameOf2(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
7961
+ const target = lazyTarget(c);
7962
+ if (target) this.assertNoRuleRef(target, seen);
7963
+ return;
7964
+ }
7965
+ for (const child of childrenOf2(c._def)) this.assertNoRuleRef(child, seen);
7966
+ }
7625
7967
  /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
7626
7968
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7627
7969
  * `g[name]` ref — only the top-level entry is unwrapped. */
@@ -7630,21 +7972,38 @@ var Serializer = class {
7630
7972
  }
7631
7973
  run() {
7632
7974
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
7975
+ for (const c of this.scanSkip) {
7976
+ this.assertNoRuleRef(c, /* @__PURE__ */ new Set());
7977
+ this.analyze(c, /* @__PURE__ */ new Set());
7978
+ }
7633
7979
  let n = 0;
7634
7980
  for (const [c, count] of this.counts) {
7635
7981
  if (this.ruleValues.has(c)) continue;
7636
7982
  if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`);
7637
7983
  }
7984
+ const outerDecls = [];
7985
+ const innerDecls = [];
7986
+ let optionsArg = "";
7987
+ if (this.scanSkip.length > 0) {
7988
+ this.decls = outerDecls;
7989
+ const units = this.scanSkip.map((c) => this.ref(c, null));
7990
+ optionsArg = `{ scanSkip: [${units.join(", ")}] }, `;
7991
+ }
7992
+ this.decls = innerDecls;
7638
7993
  const body = this.ruleMap.map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c))}`).join(",\n");
7639
- if (this.decls.length === 0) return `rules((g) => ({
7994
+ const factory = innerDecls.length === 0 ? `(g) => ({
7640
7995
  ${body}
7641
- }))`;
7642
- return `rules((g) => {
7643
- ${this.decls.map((d) => " " + d).join("\n")}
7996
+ })` : `(g) => {
7997
+ ${innerDecls.map((d) => " " + d).join("\n")}
7644
7998
  return ({
7645
7999
  ${body}
7646
8000
  })
7647
- })`;
8001
+ }`;
8002
+ const call = `rules(${optionsArg}${factory})`;
8003
+ return outerDecls.length === 0 ? call : `(() => {
8004
+ ${outerDecls.map((d) => " " + d).join("\n")}
8005
+ return ${call}
8006
+ })()`;
7648
8007
  }
7649
8008
  /** Count identity references and flag self-referential subtrees. `active` is the
7650
8009
  * set of combinators on the current DFS path (to detect a lazy pointing back). */
@@ -7729,17 +8088,21 @@ ${body}
7729
8088
  return `_gch([${items.join(", ")}])`;
7730
8089
  }
7731
8090
  case "many":
7732
- return `many(${kid(def.parser)})`;
8091
+ return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
8092
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
8093
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
7733
8094
  case "oneOrMore":
7734
- return `oneOrMore(${kid(def.parser)})`;
8095
+ return def.min === 1 && def.max === void 0 ? `oneOrMore(${kid(def.parser)})` : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
7735
8096
  case "optional":
7736
8097
  return `optional(${kid(def.parser)})`;
7737
8098
  case "attempt":
7738
8099
  return `attempt(${kid(def.parser)})`;
7739
8100
  case "sepBy":
7740
- return `sepBy(${kid(def.parser)}, ${kid(def.separator)})`;
8101
+ return `sepBy(${kid(def.parser)}, ${kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`;
7741
8102
  case "not":
7742
8103
  return `not(${kid(def.parser)})`;
8104
+ case "peek":
8105
+ return `peek(${kid(def.parser)})`;
7743
8106
  case "trivia":
7744
8107
  return `trivia(${kid(def.parser)})`;
7745
8108
  case "token":
@@ -7932,6 +8295,9 @@ var COMPOSED_COVERAGE_RULES = /* @__PURE__ */ Symbol.for("parseman.composedCover
7932
8295
  function isIRPiece(p) {
7933
8296
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
7934
8297
  }
8298
+ function carriedRuleMaps(carried) {
8299
+ return carried.filter(isIRPiece).map((p) => evalRuleMapIR(p.ir));
8300
+ }
7935
8301
  function coverageRulesOf(carried) {
7936
8302
  const winners = {};
7937
8303
  for (const piece of carried) {
@@ -7994,7 +8360,8 @@ function itemCarried(item, used, trivia2) {
7994
8360
  return false;
7995
8361
  }
7996
8362
  });
7997
- const ir = serializeRuleMap(entries);
8363
+ const scanSkip = entries.map(([, val]) => val._meta.grammarScanSkip).find(Boolean);
8364
+ const ir = serializeRuleMap(entries, scanSkip);
7998
8365
  return ir ? [{ ns, ir }] : [linkable(map, ns, trivia2)];
7999
8366
  }
8000
8367
  function composingTriviaOf(items) {
@@ -8015,6 +8382,7 @@ function compose(items) {
8015
8382
  const used = /* @__PURE__ */ new Set();
8016
8383
  const trivia2 = composingTriviaOf(items);
8017
8384
  const carried = items.flatMap((item) => itemCarried(item, used, trivia2));
8385
+ runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
8018
8386
  const pieces = carried.map((p) => materializePiece(p, trivia2));
8019
8387
  const map = fuseRules(pieces);
8020
8388
  Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false });
@@ -8847,7 +9215,7 @@ function coverageEntry(entry, collector, maps, trace) {
8847
9215
  };
8848
9216
  }
8849
9217
  case "sepBy":
8850
- return sepBy(build(def.parser), build(def.separator));
9218
+ return sepBy(build(def.parser), build(def.separator), { min: def.min, ...def.max === void 0 ? {} : { max: def.max }, ...def.trailing === void 0 ? {} : { trailing: def.trailing } });
8851
9219
  case "transform":
8852
9220
  return transform(build(def.parser), def.fn);
8853
9221
  case "skip":
@@ -8868,6 +9236,8 @@ function coverageEntry(entry, collector, maps, trace) {
8868
9236
  }, build(def.parser));
8869
9237
  case "not":
8870
9238
  return not(build(def.parser));
9239
+ case "peek":
9240
+ return peek(build(def.parser));
8871
9241
  case "node": {
8872
9242
  const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
8873
9243
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
@@ -9242,6 +9612,7 @@ export {
9242
9612
  absolutize,
9243
9613
  absolutizeCST,
9244
9614
  analyzeGating,
9615
+ analyzeGatingRules,
9245
9616
  annotateSpan,
9246
9617
  applyEdit,
9247
9618
  attempt,
@@ -9289,10 +9660,12 @@ export {
9289
9660
  not,
9290
9661
  offsetToLineCol,
9291
9662
  oneOrMore,
9663
+ oneOrMoreSep,
9292
9664
  optional,
9293
9665
  parse,
9294
9666
  parseDoc,
9295
9667
  parser,
9668
+ peek,
9296
9669
  ref,
9297
9670
  regex,
9298
9671
  relativize,