parseman 0.33.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 (50) 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.map +1 -1
  21. package/dist/compiler/linker.d.ts +5 -0
  22. package/dist/compiler/linker.d.ts.map +1 -1
  23. package/dist/compiler/value-usage.d.ts.map +1 -1
  24. package/dist/coverage.d.ts.map +1 -1
  25. package/dist/index.cjs +414 -77
  26. package/dist/index.cjs.map +4 -4
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +414 -77
  30. package/dist/index.js.map +4 -4
  31. package/dist/language-service/index.cjs.map +1 -1
  32. package/dist/language-service/index.js.map +1 -1
  33. package/dist/plugin/evaluator.d.ts.map +1 -1
  34. package/dist/plugin/index.cjs +481 -107
  35. package/dist/plugin/index.cjs.map +4 -4
  36. package/dist/plugin/index.d.ts.map +1 -1
  37. package/dist/plugin/index.js +481 -107
  38. package/dist/plugin/index.js.map +4 -4
  39. package/dist/spec/ebnf.d.ts.map +1 -1
  40. package/dist/spec/index.cjs +79 -17
  41. package/dist/spec/index.cjs.map +2 -2
  42. package/dist/spec/index.js +79 -17
  43. package/dist/spec/index.js.map +2 -2
  44. package/dist/spec/model.d.ts +23 -3
  45. package/dist/spec/model.d.ts.map +1 -1
  46. package/dist/spec/railroad.d.ts.map +1 -1
  47. package/dist/types.d.ts +9 -1
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/package.json +5 -1
package/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  absolutize: () => absolutize,
28
28
  absolutizeCST: () => absolutizeCST,
29
29
  analyzeGating: () => analyzeGating,
30
+ analyzeGatingRules: () => analyzeGatingRules,
30
31
  annotateSpan: () => annotateSpan,
31
32
  applyEdit: () => applyEdit,
32
33
  attempt: () => attempt,
@@ -74,10 +75,12 @@ __export(index_exports, {
74
75
  not: () => not,
75
76
  offsetToLineCol: () => offsetToLineCol,
76
77
  oneOrMore: () => oneOrMore,
78
+ oneOrMoreSep: () => oneOrMoreSep,
77
79
  optional: () => optional,
78
80
  parse: () => parse,
79
81
  parseDoc: () => parseDoc,
80
82
  parser: () => parser,
83
+ peek: () => peek,
81
84
  ref: () => ref,
82
85
  regex: () => regex,
83
86
  relativize: () => relativize,
@@ -102,6 +105,11 @@ __export(index_exports, {
102
105
  module.exports = __toCommonJS(index_exports);
103
106
 
104
107
  // src/combinators/first-set.ts
108
+ function resolveNamedRef(p, resolve) {
109
+ if (resolve === void 0) return void 0;
110
+ const name = p._ruleName;
111
+ return name === void 0 ? void 0 : resolve(name);
112
+ }
105
113
  function union(a, b) {
106
114
  if (a.kind === "any" || b.kind === "any") return { kind: "any" };
107
115
  if (a.kind === "empty") return b;
@@ -139,10 +147,10 @@ function any() {
139
147
  function empty() {
140
148
  return { kind: "empty" };
141
149
  }
142
- function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
150
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
143
151
  if (seen.has(p)) return true;
144
152
  seen.add(p);
145
- const me = (c) => matchesEmpty(c, seen);
153
+ const me = (c) => matchesEmpty(c, seen, resolve);
146
154
  const d = p._def;
147
155
  switch (d.tag) {
148
156
  case "literal":
@@ -159,8 +167,16 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
159
167
  case "many":
160
168
  case "optional":
161
169
  case "not":
170
+ case "peek":
162
171
  return true;
163
172
  // zero repetitions / absent / lookahead
173
+ // Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
174
+ // Any `min >= 1` requires that many ITEMS, so it is nullable only when the
175
+ // item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
176
+ // nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
177
+ // the gating diagnostic for a list that can never match empty.)
178
+ case "sepBy":
179
+ return d.min >= 1 ? me(d.parser) : true;
164
180
  case "oneOrMore":
165
181
  return me(d.parser);
166
182
  case "sequence":
@@ -184,7 +200,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
184
200
  try {
185
201
  return me(d.thunk());
186
202
  } catch {
187
- return true;
203
+ const t = resolveNamedRef(p, resolve);
204
+ return t ? me(t) : true;
188
205
  }
189
206
  default:
190
207
  return true;
@@ -193,18 +210,45 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
193
210
  function isZeroWidthAssertion(p) {
194
211
  return p._def.tag === "not";
195
212
  }
213
+ function isPositiveLookahead(p) {
214
+ return p._def.tag === "peek";
215
+ }
216
+ function narrowBy(acc, constraint) {
217
+ if (constraint.kind === "any") return acc;
218
+ if (acc === null) return constraint;
219
+ if (acc.kind === "any") return constraint;
220
+ if (acc.kind === "empty" || constraint.kind === "empty") return { kind: "empty" };
221
+ const ranges = [];
222
+ for (const a of acc.ranges) for (const b of constraint.ranges) {
223
+ const lo = Math.max(a.lo, b.lo);
224
+ const hi = Math.min(a.hi, b.hi);
225
+ if (lo <= hi) ranges.push({ lo, hi });
226
+ }
227
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
228
+ }
229
+ function applyAssertion(fs, assertion) {
230
+ if (assertion === null) return fs;
231
+ if (fs.kind === "empty") return assertion;
232
+ return narrowBy(fs, assertion) ?? fs;
233
+ }
196
234
  function sequenceFirstSet(parsers) {
197
235
  let fs = empty();
236
+ let assertion = null;
198
237
  for (const p of parsers) {
238
+ if (isPositiveLookahead(p)) {
239
+ assertion = narrowBy(assertion, p._meta.firstSet);
240
+ continue;
241
+ }
199
242
  if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet);
200
- if (!matchesEmpty(p)) return fs;
243
+ if (!matchesEmpty(p)) return applyAssertion(fs, assertion);
201
244
  }
202
- return fs;
245
+ return applyAssertion(fs, assertion);
203
246
  }
204
- function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
247
+ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve) {
205
248
  if (seen.has(p)) return any();
206
249
  seen.add(p);
207
- const fs = (c) => firstSetOf(c, seen);
250
+ const fs = (c) => firstSetOf(c, seen, resolve);
251
+ const empties = (c) => matchesEmpty(c, /* @__PURE__ */ new Set(), resolve);
208
252
  const d = p._def;
209
253
  switch (d.tag) {
210
254
  case "literal":
@@ -216,7 +260,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
216
260
  try {
217
261
  return fs(d.thunk());
218
262
  } catch {
219
- return any();
263
+ const t = resolveNamedRef(p, resolve);
264
+ return t ? fs(t) : any();
220
265
  }
221
266
  case "choice": {
222
267
  let out = empty();
@@ -225,11 +270,20 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
225
270
  }
226
271
  case "sequence": {
227
272
  let out = empty();
273
+ let assertion = null;
228
274
  for (const term of d.parsers) {
275
+ if (isPositiveLookahead(term)) {
276
+ assertion = narrowBy(assertion, fs(term));
277
+ continue;
278
+ }
229
279
  if (!isZeroWidthAssertion(term)) out = union(out, fs(term));
230
- if (!matchesEmpty(term)) return out;
280
+ if (!empties(term)) return applyAssertion(out, assertion);
231
281
  }
232
- return out;
282
+ return applyAssertion(out, assertion);
283
+ }
284
+ case "peek": {
285
+ const inner = d.parser;
286
+ return empties(inner) ? any() : fs(inner);
233
287
  }
234
288
  case "oneOrMore":
235
289
  case "many":
@@ -245,6 +299,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
245
299
  return fs(d.parser);
246
300
  case "sepBy":
247
301
  return fs(d.parser);
302
+ // both min 0 and min 1 start with the item
248
303
  case "skip":
249
304
  return fs(d.main);
250
305
  default:
@@ -929,9 +984,46 @@ function firstSetFromRegex(source) {
929
984
  return { firstSet: firstSet(ast), canMatchNewline: firstCanBeNewline(ast) };
930
985
  }
931
986
 
987
+ // src/combinators/case-fold.ts
988
+ function canonical(ch2) {
989
+ const u = ch2.toUpperCase();
990
+ if (u.length !== 1) return ch2;
991
+ if (ch2.codePointAt(0) >= 128 && u.codePointAt(0) < 128) return ch2;
992
+ return u;
993
+ }
994
+ var foldIndex = null;
995
+ function buildFoldIndex() {
996
+ const byCanonical = /* @__PURE__ */ new Map();
997
+ for (let cp = 0; cp < 65536; cp++) {
998
+ if (cp >= 55296 && cp <= 57343) continue;
999
+ const key = canonical(String.fromCharCode(cp));
1000
+ const bucket = byCanonical.get(key);
1001
+ if (bucket) bucket.push(cp);
1002
+ else byCanonical.set(key, [cp]);
1003
+ }
1004
+ const index = /* @__PURE__ */ new Map();
1005
+ for (const members of byCanonical.values()) {
1006
+ if (members.length < 2) continue;
1007
+ for (const cp of members) index.set(cp, members);
1008
+ }
1009
+ return index;
1010
+ }
1011
+ function caseFoldVariants(cp) {
1012
+ if (cp < 128) {
1013
+ if (cp >= 65 && cp <= 90) return [cp, cp + 32];
1014
+ if (cp >= 97 && cp <= 122) return [cp, cp - 32];
1015
+ return [cp];
1016
+ }
1017
+ if (cp > 65535) return [cp];
1018
+ foldIndex ??= buildFoldIndex();
1019
+ return foldIndex.get(cp) ?? [cp];
1020
+ }
1021
+
932
1022
  // src/combinators/keywords.ts
933
- function word(str, boundary = "_0-9A-Za-z") {
934
- return keywords([str], { boundary });
1023
+ function word(str, boundaryOrOpts, opts) {
1024
+ const boundary = typeof boundaryOrOpts === "string" ? boundaryOrOpts : "_0-9A-Za-z";
1025
+ const rest = typeof boundaryOrOpts === "object" && boundaryOrOpts !== null ? boundaryOrOpts : opts;
1026
+ return keywords([str], { ...rest, boundary });
935
1027
  }
936
1028
  function makeWord(boundary = "_0-9A-Za-z") {
937
1029
  return (str) => keywords([str], { boundary });
@@ -943,7 +1035,7 @@ function keywords(words, opts = {}) {
943
1035
  const sorted = [...new Set(words)].sort((a, b) => b.length - a.length);
944
1036
  const alt = sorted.map(escapeRe).join("|");
945
1037
  const boundary = opts.boundary ? `(?![${opts.boundary}])` : "";
946
- const flags = opts.caseInsensitive ? "iuy" : "uy";
1038
+ const flags = opts.caseInsensitive ? "iy" : "uy";
947
1039
  const re = new RegExp(`(?:${alt})${boundary}`, flags);
948
1040
  let firstSet2 = empty();
949
1041
  for (const w of sorted) {
@@ -951,10 +1043,7 @@ function keywords(words, opts = {}) {
951
1043
  const cp = w.codePointAt(0);
952
1044
  firstSet2 = union(firstSet2, fromChar(cp));
953
1045
  if (opts.caseInsensitive) {
954
- const u = String.fromCodePoint(cp).toUpperCase().codePointAt(0);
955
- const l = String.fromCodePoint(cp).toLowerCase().codePointAt(0);
956
- if (u !== void 0) firstSet2 = union(firstSet2, fromChar(u));
957
- if (l !== void 0) firstSet2 = union(firstSet2, fromChar(l));
1046
+ for (const v2 of caseFoldVariants(cp)) firstSet2 = union(firstSet2, fromChar(v2));
958
1047
  }
959
1048
  }
960
1049
  const meta = { firstSet: firstSet2, canMatchNewline: false, isTrivia: false };
@@ -1103,6 +1192,7 @@ function deriveExpected(c) {
1103
1192
  case "oneOrMore":
1104
1193
  case "transform":
1105
1194
  case "not":
1195
+ case "peek":
1106
1196
  return deriveExpected(def.parser);
1107
1197
  case "lazy": {
1108
1198
  const name = c._ruleName;
@@ -1576,6 +1666,13 @@ function rollbackTrivia(ctx, mark) {
1576
1666
  rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors });
1577
1667
  if (ctx._triviaLog) ctx._triviaLog.length = mark.log;
1578
1668
  }
1669
+ function saveLookaheadMark(ctx) {
1670
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null };
1671
+ }
1672
+ function rollbackLookahead(ctx, mark) {
1673
+ rollbackTrivia(ctx, mark);
1674
+ if (ctx._probe) ctx._probe.best = mark.probeBest;
1675
+ }
1579
1676
  function scanWithLabels(input, cur, ctx) {
1580
1677
  const triviaP = ctx.trivia;
1581
1678
  const spec = analyzeLabeledTrivia(triviaP);
@@ -1973,13 +2070,26 @@ function repItem(combinator, input, cur, ctx, guardable) {
1973
2070
  }
1974
2071
  return { value: result.value, end: result.span.end };
1975
2072
  }
1976
- function many(combinator) {
2073
+ function resolveBounds(what, opts) {
2074
+ const min = opts.min ?? 0;
2075
+ const max = opts.max ?? Infinity;
2076
+ const bad = (msg) => {
2077
+ throw new RangeError(`parseman: ${what} ${msg}`);
2078
+ };
2079
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`);
2080
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`);
2081
+ if (max < min) bad(`max (${max}) is less than min (${min}) \u2014 the combinator could never succeed`);
2082
+ return { min, max };
2083
+ }
2084
+ function many(combinator, opts = {}) {
2085
+ const { min, max } = resolveBounds("many()", opts);
2086
+ if (min >= 1) return atLeast(combinator, min, max);
1977
2087
  const meta = {
1978
2088
  firstSet: combinator._meta.firstSet,
1979
2089
  canMatchNewline: combinator._meta.canMatchNewline,
1980
2090
  isTrivia: false
1981
2091
  };
1982
- const def = { tag: "many", parser: combinator, min: 0 };
2092
+ const def = { tag: "many", parser: combinator, min: 0, ...max === Infinity ? {} : { max } };
1983
2093
  let expected;
1984
2094
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1985
2095
  return {
@@ -1989,7 +2099,9 @@ function many(combinator) {
1989
2099
  parse(input, pos, ctx) {
1990
2100
  const values = def.valueUnused ? void 0 : [];
1991
2101
  let cur = pos;
2102
+ let count = 0;
1992
2103
  while (cur < input.length) {
2104
+ if (count >= max) break;
1993
2105
  const item = repItem(combinator, input, cur, ctx, guardable);
1994
2106
  if (item === "stop") break;
1995
2107
  if ("fail" in item) {
@@ -2000,23 +2112,28 @@ function many(combinator) {
2000
2112
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
2001
2113
  if (values !== void 0) values.push(error);
2002
2114
  captureError(ctx, error);
2115
+ count++;
2003
2116
  cur = end;
2004
2117
  continue;
2005
2118
  }
2006
2119
  if (values !== void 0) values.push(item.value);
2120
+ count++;
2007
2121
  cur = item.end;
2008
2122
  }
2009
2123
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
2010
2124
  }
2011
2125
  };
2012
2126
  }
2013
- function oneOrMore(combinator) {
2127
+ function oneOrMore(combinator, opts = {}) {
2128
+ return many(combinator, { ...opts, min: opts.min ?? 1 });
2129
+ }
2130
+ function atLeast(combinator, min, max) {
2014
2131
  const meta = {
2015
2132
  firstSet: combinator._meta.firstSet,
2016
2133
  canMatchNewline: combinator._meta.canMatchNewline,
2017
2134
  isTrivia: false
2018
2135
  };
2019
- const def = { tag: "oneOrMore", parser: combinator, min: 1 };
2136
+ const def = { tag: "oneOrMore", parser: combinator, min, ...max === Infinity ? {} : { max } };
2020
2137
  let expected;
2021
2138
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2022
2139
  return {
@@ -2028,7 +2145,19 @@ function oneOrMore(combinator) {
2028
2145
  if (!first.ok) return first;
2029
2146
  const values = def.valueUnused ? void 0 : [first.value];
2030
2147
  let cur = first.span.end;
2148
+ let count = 1;
2149
+ while (count < min) {
2150
+ const item = repItem(combinator, input, cur, ctx, guardable);
2151
+ if (item === "stop" || "fail" in item) {
2152
+ expected ??= deriveExpected(combinator);
2153
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
2154
+ }
2155
+ if (values !== void 0) values.push(item.value);
2156
+ cur = item.end;
2157
+ count++;
2158
+ }
2031
2159
  while (cur < input.length) {
2160
+ if (count >= max) break;
2032
2161
  const item = repItem(combinator, input, cur, ctx, guardable);
2033
2162
  if (item === "stop") break;
2034
2163
  if ("fail" in item) {
@@ -2039,10 +2168,12 @@ function oneOrMore(combinator) {
2039
2168
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
2040
2169
  if (values !== void 0) values.push(error);
2041
2170
  captureError(ctx, error);
2171
+ count++;
2042
2172
  cur = end;
2043
2173
  continue;
2044
2174
  }
2045
2175
  if (values !== void 0) values.push(item.value);
2176
+ count++;
2046
2177
  cur = item.end;
2047
2178
  }
2048
2179
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
@@ -2072,17 +2203,33 @@ function optional(combinator) {
2072
2203
  }
2073
2204
  };
2074
2205
  }
2075
- function sepBy(combinator, separator) {
2206
+ function oneOrMoreSep(combinator, separator, opts = {}) {
2207
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 });
2208
+ }
2209
+ function sepBy(combinator, separator, opts = {}) {
2210
+ const { min, max } = resolveBounds("sepBy()", opts);
2211
+ const trailing = opts.trailing ?? "forbid";
2076
2212
  const meta = {
2077
2213
  firstSet: combinator._meta.firstSet,
2078
2214
  canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
2079
2215
  isTrivia: false
2080
2216
  };
2081
2217
  let expected;
2218
+ const failAt2 = (at) => {
2219
+ expected ??= deriveExpected(combinator);
2220
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } };
2221
+ };
2082
2222
  return {
2083
2223
  _tag: "sepBy",
2084
2224
  _meta: meta,
2085
- _def: { tag: "sepBy", parser: combinator, separator },
2225
+ _def: {
2226
+ tag: "sepBy",
2227
+ parser: combinator,
2228
+ separator,
2229
+ min,
2230
+ ...max === Infinity ? {} : { max },
2231
+ ...trailing === "forbid" ? {} : { trailing }
2232
+ },
2086
2233
  parse(input, pos, ctx) {
2087
2234
  const first = combinator.parse(input, pos, ctx);
2088
2235
  const values = [];
@@ -2093,6 +2240,7 @@ function sepBy(combinator, separator) {
2093
2240
  } else {
2094
2241
  const term = ctx._tolerant ? ctx._sync : void 0;
2095
2242
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2243
+ if (min >= 1) return failAt2(pos);
2096
2244
  return { ok: true, value: [], span: { start: pos, end: pos } };
2097
2245
  }
2098
2246
  expected ??= deriveExpected(combinator);
@@ -2102,6 +2250,7 @@ function sepBy(combinator, separator) {
2102
2250
  cur = rec.end;
2103
2251
  }
2104
2252
  while (cur < input.length) {
2253
+ if (values.length >= max) break;
2105
2254
  const loopMark = saveTriviaMark(ctx);
2106
2255
  let sepPos = cur;
2107
2256
  if (ctx.trivia) {
@@ -2118,6 +2267,7 @@ function sepBy(combinator, separator) {
2118
2267
  rollbackTrivia(ctx, loopMark);
2119
2268
  break;
2120
2269
  }
2270
+ const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
2121
2271
  let nextPos = sep.span.end;
2122
2272
  if (ctx.trivia) {
2123
2273
  if (needsDeferredTriviaCommit(ctx)) {
@@ -2139,12 +2289,18 @@ function sepBy(combinator, separator) {
2139
2289
  cur = rec.end;
2140
2290
  continue;
2141
2291
  }
2292
+ if (sepMark !== void 0) {
2293
+ rollbackTrivia(ctx, sepMark);
2294
+ cur = sep.span.end;
2295
+ break;
2296
+ }
2142
2297
  rollbackTrivia(ctx, loopMark);
2143
2298
  break;
2144
2299
  }
2145
2300
  values.push(next.value);
2146
2301
  cur = next.span.end;
2147
2302
  }
2303
+ if (values.length < min) return failAt2(cur);
2148
2304
  return { ok: true, value: values, span: { start: pos, end: cur } };
2149
2305
  }
2150
2306
  };
@@ -2234,6 +2390,7 @@ function markUnusedValues(root) {
2234
2390
  visit(def.separator, false);
2235
2391
  return;
2236
2392
  case "not":
2393
+ case "peek":
2237
2394
  visit(def.parser, false);
2238
2395
  return;
2239
2396
  case "grammar":
@@ -2352,9 +2509,9 @@ function not(combinator) {
2352
2509
  _meta: meta,
2353
2510
  _def: { tag: "not", parser: combinator },
2354
2511
  parse(input, pos, ctx) {
2355
- const mark = saveCstMark(ctx);
2512
+ const mark = saveLookaheadMark(ctx);
2356
2513
  const result = combinator.parse(input, pos, ctx);
2357
- rollbackCstCapture(ctx, mark);
2514
+ rollbackLookahead(ctx, mark);
2358
2515
  if (result.ok) {
2359
2516
  return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } };
2360
2517
  }
@@ -2363,6 +2520,28 @@ function not(combinator) {
2363
2520
  };
2364
2521
  }
2365
2522
 
2523
+ // src/combinators/peek.ts
2524
+ function peek(combinator) {
2525
+ const meta = {
2526
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
2527
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
2528
+ canMatchNewline: false,
2529
+ isTrivia: false
2530
+ };
2531
+ return {
2532
+ _tag: "peek",
2533
+ _meta: meta,
2534
+ _def: { tag: "peek", parser: combinator },
2535
+ parse(input, pos, ctx) {
2536
+ const mark = saveLookaheadMark(ctx);
2537
+ const result = combinator.parse(input, pos, ctx);
2538
+ rollbackLookahead(ctx, mark);
2539
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } };
2540
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } };
2541
+ }
2542
+ };
2543
+ }
2544
+
2366
2545
  // src/compiler/build-arity.ts
2367
2546
  var PARAM_LIST_RE = (
2368
2547
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
@@ -2461,6 +2640,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2461
2640
  case "expect":
2462
2641
  case "withCtx":
2463
2642
  case "not":
2643
+ case "peek":
2464
2644
  return parserHasOwnFields(d.parser, seen);
2465
2645
  default:
2466
2646
  return false;
@@ -2500,6 +2680,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2500
2680
  case "expect":
2501
2681
  case "withCtx":
2502
2682
  case "not":
2683
+ case "peek":
2503
2684
  case "field":
2504
2685
  return parserHasTriviaSite(d.parser, seen);
2505
2686
  case "choice":
@@ -2549,6 +2730,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2549
2730
  case "expect":
2550
2731
  case "withCtx":
2551
2732
  case "not":
2733
+ case "peek":
2552
2734
  return parserEnablesTriviaCapture(d.parser, seen);
2553
2735
  // leaf() clears the inner CST/trivia collector and publishes only its own
2554
2736
  // terminal result. An inner parser() capture cannot reach this node.
@@ -2924,7 +3106,7 @@ function leaf(root, fn) {
2924
3106
  }
2925
3107
 
2926
3108
  // src/version.ts
2927
- var PARSEMAN_VERSION = "0.33.0";
3109
+ var PARSEMAN_VERSION = "0.34.0";
2928
3110
 
2929
3111
  // src/analysis/gating.ts
2930
3112
  var isAny = (fs) => fs.kind === "any";
@@ -2964,14 +3146,15 @@ function firstSetToString(fs) {
2964
3146
  var ruleNameOf = (p) => p._ruleName;
2965
3147
  var SUGGESTIONS = {
2966
3148
  "broad-recognizer": "if this arm leads with a keyword regex, use word('kw', boundary) / keywords([...]) \u2014 they expose an EXACT, gating first-set and lower to the same charCodeAt scan. A genuine scanTo fallback is fine; accept it in the gating snapshot if intentional.",
2967
- "leading-not": "let the arm lead with its actual consuming terminal (first-sets gate it automatically); keep not(...) only as a TRAILING boundary. Do not hand-roll not(not(...)) to fake gating.",
2968
- "nullable-prefix": "a leading optional/many lets a later, broad term start the arm. Split the empty case into its own arm, or gate on the prefix first char.",
3149
+ "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.",
3150
+ "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.",
2969
3151
  "cross-artifact-ref": "parseman >=0.32.0 resolves a g.Foo ref first-set at fuse time; if still ANY the target rule is itself ungated \u2014 analyze it and give it a concrete non-nullable lead.",
2970
3152
  "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2971
3153
  "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2972
3154
  };
2973
- function classifyBroadArm(arm) {
3155
+ function classifyBroadArm(arm, resolve) {
2974
3156
  const seen = /* @__PURE__ */ new Set();
3157
+ const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
2975
3158
  const walk2 = (p) => {
2976
3159
  if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2977
3160
  seen.add(p);
@@ -2983,6 +3166,10 @@ function classifyBroadArm(arm) {
2983
3166
  return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2984
3167
  case "not":
2985
3168
  return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
3169
+ // peek() CARRIES its body's first-set, so it is only broad when the body is
3170
+ // (or the body is nullable) — walk in for the real cause.
3171
+ case "peek":
3172
+ return walk2(d.parser);
2986
3173
  case "scanTo":
2987
3174
  return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2988
3175
  // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
@@ -2999,18 +3186,23 @@ function classifyBroadArm(arm) {
2999
3186
  return walk2(d.parser);
3000
3187
  case "lazy": {
3001
3188
  const name = p._ruleName;
3189
+ let target;
3002
3190
  try {
3003
- const inner = walk2(d.thunk());
3004
- if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
3005
- return inner;
3191
+ target = d.thunk();
3006
3192
  } catch {
3007
- return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3193
+ const bound = name !== void 0 ? resolve?.(name) : void 0;
3194
+ if (bound === void 0)
3195
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}`, unresolvedExternal: name !== void 0 };
3196
+ target = bound;
3008
3197
  }
3198
+ const inner = walk2(target);
3199
+ if (name !== void 0) return inner ? relabel(inner, "cross-artifact-ref", `via ref g.${name} \u2192 ${inner.detail}`) : null;
3200
+ return inner;
3009
3201
  }
3010
3202
  case "optional":
3011
3203
  case "many": {
3012
3204
  const inner = walk2(d.parser);
3013
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3205
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
3014
3206
  }
3015
3207
  case "oneOrMore":
3016
3208
  case "transform":
@@ -3032,13 +3224,13 @@ function classifyBroadArm(arm) {
3032
3224
  sawNullablePrefix = true;
3033
3225
  continue;
3034
3226
  }
3035
- if (isAny(firstSetOf(t))) {
3227
+ if (isAny(firstSetOf(t, /* @__PURE__ */ new Set(), resolve))) {
3036
3228
  const inner = walk2(t);
3037
3229
  if (sawNullablePrefix)
3038
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3230
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
3039
3231
  return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
3040
3232
  }
3041
- if (matchesEmpty(t)) {
3233
+ if (matchesEmpty(t, /* @__PURE__ */ new Set(), resolve)) {
3042
3234
  sawNullablePrefix = true;
3043
3235
  continue;
3044
3236
  }
@@ -3049,7 +3241,7 @@ function classifyBroadArm(arm) {
3049
3241
  case "choice": {
3050
3242
  for (const a of d.parsers) {
3051
3243
  const r = walk2(a);
3052
- if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3244
+ if (r) return relabel(r, r.cause, `choice arm \u2192 ${r.detail}`);
3053
3245
  }
3054
3246
  return null;
3055
3247
  }
@@ -3086,7 +3278,7 @@ function detectAntiPatterns(rule, arms) {
3086
3278
  kind: "double-not",
3087
3279
  rule,
3088
3280
  armIndex: i,
3089
- message: "not(not(...)) hand-rolls automatic first-char gating and MISCOMPILES among shared-first-char sibling arms (its first-set is ANY, poisoning dispatch). Remove it; let the arm lead with its consuming terminal, or use word()/keywords() for an exact first-set."
3281
+ 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."
3090
3282
  });
3091
3283
  } else {
3092
3284
  out.push({
@@ -3109,6 +3301,9 @@ function detectAntiPatterns(rule, arms) {
3109
3301
  return out;
3110
3302
  }
3111
3303
  function analyzeGating(entry, opts) {
3304
+ return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3305
+ }
3306
+ function analyzeGatingRules(ruleMap, opts) {
3112
3307
  const raw = [];
3113
3308
  const antiPatterns = [];
3114
3309
  const visited = /* @__PURE__ */ new Set();
@@ -3118,7 +3313,7 @@ function analyzeGating(entry, opts) {
3118
3313
  const d = p._def;
3119
3314
  const rule = ruleNameOf(p) ?? enclosingRule;
3120
3315
  if (d.tag === "choice") {
3121
- raw.push({ g: analyzeChoice(p, d, rule), rule });
3316
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
3122
3317
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3123
3318
  }
3124
3319
  const rec = d;
@@ -3135,7 +3330,7 @@ function analyzeGating(entry, opts) {
3135
3330
  }
3136
3331
  for (const k of kids) visit(k, rule);
3137
3332
  };
3138
- visit(entry, ruleNameOf(entry) ?? "<entry>");
3333
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3139
3334
  const perRule = /* @__PURE__ */ new Map();
3140
3335
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3141
3336
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3151,18 +3346,19 @@ function analyzeGating(entry, opts) {
3151
3346
  });
3152
3347
  const gated = choices.filter((c) => c.gates === "yes").length;
3153
3348
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3154
- const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3349
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted && !c.deferred);
3155
3350
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3351
+ const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3156
3352
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3157
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3353
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3158
3354
  }
3159
- function analyzeChoice(p, d, rule) {
3355
+ function analyzeChoice(p, d, rule, resolve) {
3160
3356
  const arms = d.parsers;
3161
3357
  const shallow = arms.map((a) => a._meta.firstSet);
3162
- const deep = arms.map((a) => firstSetOf(a));
3358
+ const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
3163
3359
  const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3164
- const { cause, detail } = classifyBroadArm(arms[index]);
3165
- return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3360
+ const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
3361
+ return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
3166
3362
  });
3167
3363
  const overlaps = [];
3168
3364
  for (let i = 0; i < deep.length; i++)
@@ -3178,10 +3374,12 @@ function analyzeChoice(p, d, rule) {
3178
3374
  if (d.disjoint) gates = "yes";
3179
3375
  else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3180
3376
  else gates = "no";
3377
+ const deferred = gates === "no" && overlaps.length === 0 && anyArms.length > 0 && anyArms.every((a) => a.unresolvedExternal);
3181
3378
  return {
3182
3379
  rule,
3183
3380
  strategy: d.strategy?.tag ?? "firstMatch",
3184
3381
  gates,
3382
+ deferred,
3185
3383
  combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3186
3384
  anyArms,
3187
3385
  overlaps
@@ -3348,6 +3546,7 @@ function children(def, winners) {
3348
3546
  case "field":
3349
3547
  case "grammar":
3350
3548
  case "not":
3549
+ case "peek":
3351
3550
  case "node":
3352
3551
  case "guard":
3353
3552
  case "withCtx":
@@ -5113,6 +5312,9 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5113
5312
  case "keywords":
5114
5313
  case "guard":
5115
5314
  case "not":
5315
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
5316
+ // outcomes, so it can never leave a partial capture behind.
5317
+ case "peek":
5116
5318
  case "trivia":
5117
5319
  case "token":
5118
5320
  case "leaf":
@@ -5184,6 +5386,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
5184
5386
  case "leaf":
5185
5387
  return true;
5186
5388
  case "not":
5389
+ case "peek":
5187
5390
  case "guard":
5188
5391
  case "trivia":
5189
5392
  case "unknown":
@@ -5310,6 +5513,7 @@ function emitKeywordsFast(def, ctx, pos) {
5310
5513
  if (def.words.length === 0 || def.words.some((w) => w.length === 0)) return null;
5311
5514
  if (def.words.some((w) => Array.from(w).length !== w.length)) return null;
5312
5515
  if (def.caseInsensitive && def.boundary) return null;
5516
+ if (def.caseInsensitive && def.words.some((w) => w.split("").some((c) => c.charCodeAt(0) > 127))) return null;
5313
5517
  let boundary = null;
5314
5518
  if (def.boundary) {
5315
5519
  let body = def.boundary;
@@ -5357,7 +5561,7 @@ function emitKeywords(def, ctx, pos) {
5357
5561
  if (fast) return fast;
5358
5562
  const alt = def.words.map(escapeKeywordRe).join("|");
5359
5563
  const boundary = def.boundary ? `(?![${def.boundary}])` : "";
5360
- const flags = def.caseInsensitive ? "iuy" : "uy";
5564
+ const flags = def.caseInsensitive ? "iy" : "uy";
5361
5565
  const source = `(?:${alt})${boundary}`;
5362
5566
  const key = `${source}/${flags}`;
5363
5567
  let rName = ctx.regexMap.get(key);
@@ -5503,6 +5707,7 @@ function failsAtStart(p) {
5503
5707
  case "keywords":
5504
5708
  case "guard":
5505
5709
  case "not":
5710
+ case "peek":
5506
5711
  return true;
5507
5712
  case "transform":
5508
5713
  case "label":
@@ -5916,14 +6121,17 @@ function emitMany(def, ctx, pos) {
5916
6121
  ];
5917
6122
  const mySyncV = ctx.recovery ? v(ctx, "_mysy") : "";
5918
6123
  if (ctx.recovery) stmts.push(`${ind(ctx)}const ${mySyncV} = _ctx._sync`);
5919
- if (def.min === 1) {
6124
+ for (let i = 0; i < def.min; i++) {
5920
6125
  const firstR = emit(def.parser, ctx, curV);
5921
6126
  stmts.push(...firstR.stmts);
5922
6127
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
5923
6128
  stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
5924
6129
  }
6130
+ const maxV = def.max === void 0 ? null : wantValue ? `${arrV}.length` : v(ctx, "_cnt");
6131
+ if (maxV !== null && !wantValue) stmts.push(`${ind(ctx)}let ${maxV} = ${def.min}`);
5925
6132
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
5926
6133
  ctx.indent++;
6134
+ if (maxV !== null) stmts.push(`${ind(ctx)}if (${maxV} >= ${def.max}) break`);
5927
6135
  let itemPos = curV;
5928
6136
  let rollback = "";
5929
6137
  if (ctx.activeTrivia) {
@@ -5964,7 +6172,7 @@ function emitMany(def, ctx, pos) {
5964
6172
  `${ind(ctx)}if (!${iterOk}) {`,
5965
6173
  `${ind(ctx)} ${rollback}if (_ctx._tolerant && ${mySyncV} !== undefined && !_ctx._rec.at(${mySyncV}, input, ${itemPos}, _ctx)) {`,
5966
6174
  `${ind(ctx)} const ${rrV} = _ctx._rec.scan(input, ${itemPos}, _ctx, ${mySyncV}, ${exp})`,
5967
- ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : [],
6175
+ ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : maxV !== null ? [`${ind(ctx)} ${maxV}++`] : [],
5968
6176
  `${ind(ctx)} _ctx._rec.capture(_ctx, ${rrV}.error)`,
5969
6177
  `${ind(ctx)} ${curV} = ${rrV}.end`,
5970
6178
  `${ind(ctx)} continue`,
@@ -5977,6 +6185,7 @@ function emitMany(def, ctx, pos) {
5977
6185
  stmts.push(`${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`);
5978
6186
  }
5979
6187
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
6188
+ else if (maxV !== null) stmts.push(`${ind(ctx)}${maxV}++`);
5980
6189
  stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
5981
6190
  ctx.indent--;
5982
6191
  stmts.push(`${ind(ctx)}}`);
@@ -6041,8 +6250,8 @@ function emitSepBy(_p, def, ctx, pos) {
6041
6250
  ...rec ? [`${ind(ctx)}const ${mySyncV} = _ctx._sync`] : [],
6042
6251
  ...firstStmts
6043
6252
  ];
6044
- const failItem = (nextOkVar, itemPosVar, rb, originalFail) => {
6045
- if (!rec) return [originalFail];
6253
+ const failItem = (nextOkVar, itemPosVar, breakStmt) => {
6254
+ if (!rec) return [`${ind(ctx)}if (!${nextOkVar}) ${breakStmt === "break" ? "break" : `{ ${breakStmt} }`}`];
6046
6255
  const rr = v(ctx, "_rr");
6047
6256
  return [
6048
6257
  `${ind(ctx)}if (!${nextOkVar}) {`,
@@ -6050,10 +6259,24 @@ function emitSepBy(_p, def, ctx, pos) {
6050
6259
  `${ind(ctx)} const ${rr} = _ctx._rec.scan(input, ${itemPosVar}, _ctx, ${scanSync}, ${exp})`,
6051
6260
  `${ind(ctx)} ${arrV}.push(${rr}.error); _ctx._rec.capture(_ctx, ${rr}.error); ${curV} = ${rr}.end; continue`,
6052
6261
  `${ind(ctx)} }`,
6053
- `${ind(ctx)} ${rb}break`,
6262
+ `${ind(ctx)} ${breakStmt}`,
6054
6263
  `${ind(ctx)}}`
6055
6264
  ];
6056
6265
  };
6266
+ const itemFailBreak = (sepEndVar, sepRb, postSepRb) => def.trailing === void 0 ? `${sepRb}break` : `${postSepRb}${curV} = ${sepEndVar}; break`;
6267
+ const postSepMarks = () => {
6268
+ if (def.trailing === void 0 || !ctx.capturing) return { decl: [], rb: "" };
6269
+ const lv = v(ctx, "_tlv"), rw = v(ctx, "_trw"), tl = v(ctx, "_ttl"), lg = v(ctx, "_tlg");
6270
+ return {
6271
+ decl: [
6272
+ `${ind(ctx)}const ${lv} = _ctx._cstLeaves ? _ctx._cstLeaves.length : 0`,
6273
+ `${ind(ctx)}const ${rw} = _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0`,
6274
+ `${ind(ctx)}const ${tl} = _ctx._cstTriviaLog ? _ctx._cstTriviaLog.length : 0`,
6275
+ `${ind(ctx)}const ${lg} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`
6276
+ ],
6277
+ 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}; `
6278
+ };
6279
+ };
6057
6280
  if (rec) {
6058
6281
  const rr0 = v(ctx, "_rr0");
6059
6282
  const didV = v(ctx, "_did");
@@ -6079,6 +6302,7 @@ function emitSepBy(_p, def, ctx, pos) {
6079
6302
  );
6080
6303
  ctx.indent++;
6081
6304
  }
6305
+ if (def.max !== void 0) stmts.push(`${ind(ctx)}if (${arrV}.length >= ${def.max}) break`);
6082
6306
  let sepAtPos = curV;
6083
6307
  if (ctx.activeTrivia) {
6084
6308
  if (ctx.capturing) {
@@ -6099,12 +6323,14 @@ function emitSepBy(_p, def, ctx, pos) {
6099
6323
  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}; `;
6100
6324
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6101
6325
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) { ${rollbackToSep}break }`);
6326
+ const post = postSepMarks();
6327
+ stmts.push(...post.decl);
6102
6328
  const npV = v(ctx, "_np");
6103
6329
  stmts.push(`${ind(ctx)}const ${npV} = ${capFn}(input, ${sepEnd}, _ctx, 1)`);
6104
6330
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6105
6331
  stmts.push(
6106
6332
  ...nextStmts,
6107
- ...failItem(nextOk, npV, rollbackToSep, `${ind(ctx)}if (!${nextOk}) { ${rollbackToSep}break }`),
6333
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, rollbackToSep, post.rb)),
6108
6334
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6109
6335
  `${ind(ctx)}${curV} = ${nextEnd}`
6110
6336
  );
@@ -6120,7 +6346,7 @@ function emitSepBy(_p, def, ctx, pos) {
6120
6346
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6121
6347
  stmts.push(
6122
6348
  ...nextStmts,
6123
- ...failItem(nextOk, npV, "", `${ind(ctx)}if (!${nextOk}) break`),
6349
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, "", "")),
6124
6350
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6125
6351
  `${ind(ctx)}${curV} = ${nextEnd}`
6126
6352
  );
@@ -6137,10 +6363,12 @@ function emitSepBy(_p, def, ctx, pos) {
6137
6363
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6138
6364
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) break`);
6139
6365
  const nextRb = markLv ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6366
+ const post = postSepMarks();
6367
+ stmts.push(...post.decl);
6140
6368
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, sepEnd, true);
6141
6369
  stmts.push(
6142
6370
  ...nextStmts,
6143
- ...failItem(nextOk, sepEnd, nextRb, `${ind(ctx)}if (!${nextOk}) { ${nextRb}break }`),
6371
+ ...failItem(nextOk, sepEnd, itemFailBreak(sepEnd, nextRb, post.rb)),
6144
6372
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6145
6373
  `${ind(ctx)}${curV} = ${nextEnd}`
6146
6374
  );
@@ -6149,6 +6377,9 @@ function emitSepBy(_p, def, ctx, pos) {
6149
6377
  stmts.push(`${ind(ctx)}}`);
6150
6378
  ctx.indent--;
6151
6379
  stmts.push(`${ind(ctx)}}`);
6380
+ if (def.min >= 1) {
6381
+ stmts.push(...emitIfFail(ctx, `${arrV}.length < ${def.min}`, failArrBody(ctx, deriveExpectedArr([def.parser]), curV)));
6382
+ }
6152
6383
  return { stmts, valueVar: arrV, endVar: curV };
6153
6384
  }
6154
6385
  function emitScanTo(def, ctx, pos) {
@@ -6200,17 +6431,44 @@ function emitScanTo(def, ctx, pos) {
6200
6431
  return { stmts, valueVar: valV, endVar: curV };
6201
6432
  }
6202
6433
  function emitNot(def, ctx, pos) {
6434
+ const sinksLive = !!ctx.capturing || !!ctx.recovery;
6435
+ const leaves = sinksLive ? v(ctx, "_ntl") : null;
6436
+ const raw = sinksLive ? v(ctx, "_ntr") : null;
6437
+ const tl = sinksLive ? v(ctx, "_ntt") : null;
6438
+ const log = sinksLive ? v(ctx, "_ntg") : null;
6439
+ const fields = sinksLive ? v(ctx, "_ntf") : null;
6440
+ const errors = sinksLive ? v(ctx, "_nte") : null;
6203
6441
  const { stmts, okVar } = emitFallible(def.parser, ctx, pos, true);
6204
6442
  const label2 = JSON.stringify(`not(${def.parser._tag})`);
6205
6443
  return {
6206
6444
  stmts: [
6445
+ ...sinksLive ? [
6446
+ `${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`
6447
+ ] : [],
6207
6448
  ...stmts,
6449
+ ...sinksLive ? [
6450
+ `${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}`
6451
+ ] : [],
6208
6452
  ...emitIfFail(ctx, okVar, failBody(ctx, label2, pos))
6209
6453
  ],
6210
6454
  valueVar: "null",
6211
6455
  endVar: pos
6212
6456
  };
6213
6457
  }
6458
+ function emitPeek(def, ctx, pos) {
6459
+ const probeCtx = { ...ctx, capturing: false, noHoist: true };
6460
+ const { stmts, okVar } = emitFallible(def.parser, probeCtx, pos, true);
6461
+ ctx.vars = probeCtx.vars;
6462
+ const label2 = JSON.stringify(`peek(${def.parser._tag})`);
6463
+ return {
6464
+ stmts: [
6465
+ ...stmts,
6466
+ ...emitIfFail(ctx, `!${okVar}`, failBody(ctx, label2, pos))
6467
+ ],
6468
+ valueVar: "null",
6469
+ endVar: pos
6470
+ };
6471
+ }
6214
6472
  function escapeTokenLiteral(value) {
6215
6473
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6216
6474
  }
@@ -6498,9 +6756,9 @@ function emitRuntimeFallback(parser2, ctx, pos) {
6498
6756
  return { stmts, valueVar: vv, endVar: ev };
6499
6757
  }
6500
6758
  function emitLazy(p, def, ctx, pos) {
6501
- const canonical = ctx.ruleNames?.get(p);
6759
+ const canonical2 = ctx.ruleNames?.get(p);
6502
6760
  const usage = ctx.lazyUsage;
6503
- if (!canonical && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6761
+ if (!canonical2 && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6504
6762
  let resolved;
6505
6763
  try {
6506
6764
  resolved = def.thunk();
@@ -6510,7 +6768,7 @@ function emitLazy(p, def, ctx, pos) {
6510
6768
  return emit(resolved, ctx, pos);
6511
6769
  }
6512
6770
  if (!ctx.namedParsers.has(p)) {
6513
- const fnName2 = canonical ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6771
+ const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6514
6772
  ctx.namedParsers.set(p, fnName2);
6515
6773
  let resolved;
6516
6774
  try {
@@ -6793,6 +7051,8 @@ function emitDispatch(p, ctx, pos) {
6793
7051
  }
6794
7052
  case "not":
6795
7053
  return emitNot(def, ctx, pos);
7054
+ case "peek":
7055
+ return emitPeek(def, ctx, pos);
6796
7056
  case "node":
6797
7057
  return emitNode(def, ctx, pos);
6798
7058
  case "scanTo":
@@ -6881,6 +7141,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
6881
7141
  case "many":
6882
7142
  case "oneOrMore":
6883
7143
  case "not":
7144
+ case "peek":
6884
7145
  case "transform":
6885
7146
  return hasNodeDef(d.parser, seen);
6886
7147
  case "skip":
@@ -6932,17 +7193,18 @@ function childrenOf(def) {
6932
7193
  case "field":
6933
7194
  case "grammar":
6934
7195
  case "not":
7196
+ case "peek":
6935
7197
  case "node":
6936
7198
  case "withCtx":
6937
7199
  case "expect":
6938
7200
  return [def.parser];
6939
- // emitMany's oneOrMore branch and emitSepBy both codegen `def.parser` TWICE
6940
- // (a mandatory first match, then again inside the repeat loop) — two real
6941
- // emit() call sites, so the usage analysis must see two edges here or it
7201
+ // emitMany's min>=1 branch and emitSepBy both codegen `def.parser` more than
7202
+ // once (`min` mandatory matches, then again inside the repeat loop) — each is a
7203
+ // real emit() call site, so the usage analysis must see one edge per site or it
6942
7204
  // undercounts a single-use `parser` ref as inline-safe when it's actually
6943
- // referenced from two positions within this one compiled function.
7205
+ // referenced from several positions within this one compiled function.
6944
7206
  case "oneOrMore":
6945
- return [def.parser, def.parser];
7207
+ return Array.from({ length: def.min + 1 }, () => def.parser);
6946
7208
  case "sepBy":
6947
7209
  return [def.parser, def.parser, def.separator];
6948
7210
  case "skip":
@@ -7088,12 +7350,41 @@ function resolveGatingLevel(opt) {
7088
7350
  return "warn";
7089
7351
  }
7090
7352
  function runGatingDiagnostic(combinator, opt) {
7353
+ return reportGating(opt, (o) => analyzeGating(combinator, o));
7354
+ }
7355
+ function runGatingDiagnosticRules(ruleMap, opt) {
7356
+ return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7357
+ }
7358
+ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7359
+ if (resolveGatingLevel(opt) === "off") return void 0;
7360
+ const ruleMaps = getRuleMaps();
7361
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
7362
+ const winners = /* @__PURE__ */ new Map();
7363
+ for (const map of ruleMaps) for (const [name, rule] of map) {
7364
+ if (rule._def.tag === "lazy") {
7365
+ try {
7366
+ rule._def.thunk();
7367
+ } catch {
7368
+ continue;
7369
+ }
7370
+ }
7371
+ winners.set(name, rule);
7372
+ }
7373
+ const entries = [...winners];
7374
+ return reportGating(opt, (o) => {
7375
+ const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7376
+ const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7377
+ return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
7378
+ });
7379
+ }
7380
+ function reportGating(opt, analyze) {
7091
7381
  const level = resolveGatingLevel(opt);
7092
7382
  if (level === "off") return void 0;
7093
- const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7383
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
7384
+ 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;
7094
7385
  let report;
7095
7386
  try {
7096
- report = analyzeGating(combinator, accept ? { accept } : void 0);
7387
+ report = analyze(analyzeOpts);
7097
7388
  } catch {
7098
7389
  return void 0;
7099
7390
  }
@@ -7230,6 +7521,7 @@ function publicRuleWrapperSource(rule, fnSource) {
7230
7521
  }
7231
7522
  function compileLinkable(ruleMapArg, ns, opts) {
7232
7523
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7524
+ if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
7233
7525
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7234
7526
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7235
7527
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
@@ -7286,6 +7578,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7286
7578
  fnNameToKey.set(fn, key);
7287
7579
  }
7288
7580
  ctx.ruleNames = ruleNames;
7581
+ const externalRefs = /* @__PURE__ */ new Set();
7289
7582
  {
7290
7583
  const scanned = /* @__PURE__ */ new Set();
7291
7584
  const scanExternal = (p) => {
@@ -7301,11 +7594,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
7301
7594
  }
7302
7595
  if (resolved === void 0) {
7303
7596
  const name = p._ruleName;
7304
- if (name && !ctx.namedParsers.has(p)) {
7305
- assertRuleName(name);
7306
- const fn = `_r_${name}`;
7307
- ruleNames.set(p, fn);
7308
- ctx.namedParsers.set(p, fn);
7597
+ if (name) {
7598
+ externalRefs.add(p);
7599
+ if (!ctx.namedParsers.has(p)) {
7600
+ assertRuleName(name);
7601
+ const fn = `_r_${name}`;
7602
+ ruleNames.set(p, fn);
7603
+ ctx.namedParsers.set(p, fn);
7604
+ }
7309
7605
  }
7310
7606
  return;
7311
7607
  }
@@ -7418,12 +7714,32 @@ function compileLinkable(ruleMapArg, ns, opts) {
7418
7714
  needsEmptyTl: !!ctx.needsEmptyTl,
7419
7715
  needsHostReads: !!ctx.needsHostReads,
7420
7716
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7421
- isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule)),
7717
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7422
7718
  mfFns: mfSrcs ? [] : ctx.mapFns,
7423
7719
  buildFns: buildSrcs ? [] : ctx.buildFns
7424
7720
  };
7425
7721
  }
7426
- function hasSemanticReduction(roots) {
7722
+ function hasExternalRuleRef(ruleMap) {
7723
+ const seen = /* @__PURE__ */ new Set();
7724
+ const visit = (p) => {
7725
+ if (seen.has(p)) return false;
7726
+ seen.add(p);
7727
+ const def = p._def;
7728
+ if (def.tag === "lazy") {
7729
+ let resolved;
7730
+ try {
7731
+ resolved = def.thunk();
7732
+ } catch {
7733
+ resolved = void 0;
7734
+ }
7735
+ if (resolved === void 0) return p._ruleName !== void 0;
7736
+ return visit(resolved);
7737
+ }
7738
+ return childrenOf(def).some(visit);
7739
+ };
7740
+ return ruleMap.some(([, rule]) => visit(rule));
7741
+ }
7742
+ function hasSemanticReduction(roots, externalRefs) {
7427
7743
  const seen = /* @__PURE__ */ new Set();
7428
7744
  const visit = (parser2) => {
7429
7745
  if (seen.has(parser2)) return false;
@@ -7434,6 +7750,7 @@ function hasSemanticReduction(roots) {
7434
7750
  if (def.tag === "guard" || def.tag === "withCtx") return true;
7435
7751
  if (def.tag === "node" && def.build !== void 0) return true;
7436
7752
  if (def.tag === "lazy") {
7753
+ if (externalRefs?.has(parser2)) return false;
7437
7754
  try {
7438
7755
  return visit(def.thunk());
7439
7756
  } catch {
@@ -7525,6 +7842,7 @@ function childrenOf2(def) {
7525
7842
  case "label":
7526
7843
  case "field":
7527
7844
  case "not":
7845
+ case "peek":
7528
7846
  case "node":
7529
7847
  case "withCtx":
7530
7848
  case "expect":
@@ -7558,6 +7876,13 @@ function lazyTarget(c) {
7558
7876
  }
7559
7877
  var Unserializable = class extends Error {
7560
7878
  };
7879
+ function repeatOpts(min, max, trailing) {
7880
+ const parts = [];
7881
+ if (min !== 0) parts.push(`min: ${min}`);
7882
+ if (max !== void 0) parts.push(`max: ${max}`);
7883
+ if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
7884
+ return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
7885
+ }
7561
7886
  function serializeRuleMap(ruleMap, scanSkip) {
7562
7887
  try {
7563
7888
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -7661,6 +7986,7 @@ function evalRuleMapIR(ir) {
7661
7986
  "optional",
7662
7987
  "sepBy",
7663
7988
  "not",
7989
+ "peek",
7664
7990
  "node",
7665
7991
  "parser",
7666
7992
  "scanTo",
@@ -7693,6 +8019,7 @@ function evalRuleMapIR(ir) {
7693
8019
  optional,
7694
8020
  sepBy,
7695
8021
  not,
8022
+ peek,
7696
8023
  node,
7697
8024
  parser,
7698
8025
  scanTo,
@@ -7867,17 +8194,21 @@ ${outerDecls.map((d) => " " + d).join("\n")}
7867
8194
  return `_gch([${items.join(", ")}])`;
7868
8195
  }
7869
8196
  case "many":
7870
- return `many(${kid(def.parser)})`;
8197
+ return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
8198
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
8199
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
7871
8200
  case "oneOrMore":
7872
- return `oneOrMore(${kid(def.parser)})`;
8201
+ return def.min === 1 && def.max === void 0 ? `oneOrMore(${kid(def.parser)})` : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
7873
8202
  case "optional":
7874
8203
  return `optional(${kid(def.parser)})`;
7875
8204
  case "attempt":
7876
8205
  return `attempt(${kid(def.parser)})`;
7877
8206
  case "sepBy":
7878
- return `sepBy(${kid(def.parser)}, ${kid(def.separator)})`;
8207
+ return `sepBy(${kid(def.parser)}, ${kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`;
7879
8208
  case "not":
7880
8209
  return `not(${kid(def.parser)})`;
8210
+ case "peek":
8211
+ return `peek(${kid(def.parser)})`;
7881
8212
  case "trivia":
7882
8213
  return `trivia(${kid(def.parser)})`;
7883
8214
  case "token":
@@ -8070,6 +8401,9 @@ var COMPOSED_COVERAGE_RULES = /* @__PURE__ */ Symbol.for("parseman.composedCover
8070
8401
  function isIRPiece(p) {
8071
8402
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
8072
8403
  }
8404
+ function carriedRuleMaps(carried) {
8405
+ return carried.filter(isIRPiece).map((p) => evalRuleMapIR(p.ir));
8406
+ }
8073
8407
  function coverageRulesOf(carried) {
8074
8408
  const winners = {};
8075
8409
  for (const piece of carried) {
@@ -8154,6 +8488,7 @@ function compose(items) {
8154
8488
  const used = /* @__PURE__ */ new Set();
8155
8489
  const trivia2 = composingTriviaOf(items);
8156
8490
  const carried = items.flatMap((item) => itemCarried(item, used, trivia2));
8491
+ runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
8157
8492
  const pieces = carried.map((p) => materializePiece(p, trivia2));
8158
8493
  const map = fuseRules(pieces);
8159
8494
  Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false });
@@ -8986,7 +9321,7 @@ function coverageEntry(entry, collector, maps, trace) {
8986
9321
  };
8987
9322
  }
8988
9323
  case "sepBy":
8989
- return sepBy(build(def.parser), build(def.separator));
9324
+ 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 } });
8990
9325
  case "transform":
8991
9326
  return transform(build(def.parser), def.fn);
8992
9327
  case "skip":
@@ -9007,6 +9342,8 @@ function coverageEntry(entry, collector, maps, trace) {
9007
9342
  }, build(def.parser));
9008
9343
  case "not":
9009
9344
  return not(build(def.parser));
9345
+ case "peek":
9346
+ return peek(build(def.parser));
9010
9347
  case "node": {
9011
9348
  const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
9012
9349
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);