parseman 0.33.0 → 0.35.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 (59) hide show
  1. package/README.md +94 -72
  2. package/dist/analysis/gating.d.ts +66 -1
  3. package/dist/analysis/gating.d.ts.map +1 -1
  4. package/dist/combinators/case-fold.d.ts +39 -0
  5. package/dist/combinators/case-fold.d.ts.map +1 -0
  6. package/dist/combinators/expect.d.ts +1 -1
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/first-set.d.ts +38 -11
  9. package/dist/combinators/first-set.d.ts.map +1 -1
  10. package/dist/combinators/keywords.d.ts +12 -3
  11. package/dist/combinators/keywords.d.ts.map +1 -1
  12. package/dist/combinators/not.d.ts.map +1 -1
  13. package/dist/combinators/peek.d.ts +27 -0
  14. package/dist/combinators/peek.d.ts.map +1 -0
  15. package/dist/combinators/repeat.d.ts +68 -3
  16. package/dist/combinators/repeat.d.ts.map +1 -1
  17. package/dist/combinators/trivia-skip.d.ts +23 -1
  18. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  19. package/dist/compiler/codegen.d.ts +40 -0
  20. package/dist/compiler/codegen.d.ts.map +1 -1
  21. package/dist/compiler/fields.d.ts.map +1 -1
  22. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  23. package/dist/compiler/linker.d.ts +5 -0
  24. package/dist/compiler/linker.d.ts.map +1 -1
  25. package/dist/compiler/value-usage.d.ts.map +1 -1
  26. package/dist/coverage.d.ts.map +1 -1
  27. package/dist/cst/capture-buffer.d.ts.map +1 -1
  28. package/dist/index.cjs +462 -105
  29. package/dist/index.cjs.map +4 -4
  30. package/dist/index.d.ts +4 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +462 -105
  33. package/dist/index.js.map +4 -4
  34. package/dist/language-service/index.cjs.map +1 -1
  35. package/dist/language-service/index.js.map +1 -1
  36. package/dist/plugin/evaluator.d.ts.map +1 -1
  37. package/dist/plugin/index.cjs +529 -135
  38. package/dist/plugin/index.cjs.map +4 -4
  39. package/dist/plugin/index.d.ts.map +1 -1
  40. package/dist/plugin/index.js +529 -135
  41. package/dist/plugin/index.js.map +4 -4
  42. package/dist/run/index.cjs +216 -0
  43. package/dist/run/index.cjs.map +7 -0
  44. package/dist/run/index.d.ts +28 -0
  45. package/dist/run/index.d.ts.map +1 -0
  46. package/dist/run/index.js +193 -0
  47. package/dist/run/index.js.map +7 -0
  48. package/dist/spec/ebnf.d.ts.map +1 -1
  49. package/dist/spec/index.cjs +79 -17
  50. package/dist/spec/index.cjs.map +2 -2
  51. package/dist/spec/index.js +79 -17
  52. package/dist/spec/index.js.map +2 -2
  53. package/dist/spec/model.d.ts +23 -3
  54. package/dist/spec/model.d.ts.map +1 -1
  55. package/dist/spec/railroad.d.ts.map +1 -1
  56. package/dist/types.d.ts +9 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/package.json +11 -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:
@@ -386,28 +441,28 @@ function rollbackBufList(b, keyMulti, keySingle, len) {
386
441
  else if (len === 1) {
387
442
  b[keySingle] = arr[0];
388
443
  b[keyMulti] = void 0;
389
- } else arr.length = len;
444
+ } else if (arr.length !== len) arr.length = len;
390
445
  return;
391
446
  }
392
447
  if (len === 0) b[keySingle] = void 0;
393
448
  }
394
449
  function rollbackCstCapture(ctx, mark) {
395
- if (ctx._errors && mark.errors !== void 0) ctx._errors.length = mark.errors;
450
+ if (ctx._errors && mark.errors !== void 0 && ctx._errors.length !== mark.errors) ctx._errors.length = mark.errors;
396
451
  const b = ctx._cstBuf;
397
452
  if (b) {
398
453
  rollbackBufList(b, "raw", "rawSingle", mark.raw);
399
454
  rollbackBufList(b, "ch", "single", mark.leaves);
400
455
  if (b.tl) {
401
456
  if (mark.tlog === 0) b.tl = void 0;
402
- else b.tl.length = mark.tlog;
457
+ else if (b.tl.length !== mark.tlog) b.tl.length = mark.tlog;
403
458
  }
404
- if (ctx._fields) ctx._fields.length = mark.fields;
459
+ if (ctx._fields && ctx._fields.length !== mark.fields) ctx._fields.length = mark.fields;
405
460
  return;
406
461
  }
407
- if (ctx._cstRawChildren) ctx._cstRawChildren.length = mark.raw;
408
- if (ctx._cstTriviaLog) ctx._cstTriviaLog.length = mark.tlog;
409
- if (ctx._cstLeaves) ctx._cstLeaves.length = mark.leaves;
410
- if (ctx._fields) ctx._fields.length = mark.fields;
462
+ if (ctx._cstRawChildren && ctx._cstRawChildren.length !== mark.raw) ctx._cstRawChildren.length = mark.raw;
463
+ if (ctx._cstTriviaLog && ctx._cstTriviaLog.length !== mark.tlog) ctx._cstTriviaLog.length = mark.tlog;
464
+ if (ctx._cstLeaves && ctx._cstLeaves.length !== mark.leaves) ctx._cstLeaves.length = mark.leaves;
465
+ if (ctx._fields && ctx._fields.length !== mark.fields) ctx._fields.length = mark.fields;
411
466
  }
412
467
  function pushCstTriviaEntry(ctx, start, end, kindIndex) {
413
468
  const insertIdx = cstRawLen(ctx);
@@ -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 };
@@ -1066,6 +1155,10 @@ var REC = { scan: recoverScan, at: matchesAt, or: orSentinel, sentinel: firstSet
1066
1155
 
1067
1156
  // src/combinators/expect.ts
1068
1157
  function deriveExpected(c) {
1158
+ return derive(c, /* @__PURE__ */ new Set());
1159
+ }
1160
+ function derive(c, seen) {
1161
+ const deriveExpected2 = (p) => derive(p, seen);
1069
1162
  const def = c._def;
1070
1163
  switch (def.tag) {
1071
1164
  case "literal":
@@ -1077,20 +1170,32 @@ function deriveExpected(c) {
1077
1170
  case "label":
1078
1171
  return [def.label];
1079
1172
  case "choice":
1080
- return def.parsers.flatMap(deriveExpected);
1081
- case "sequence":
1082
- return def.parsers.length > 0 ? deriveExpected(def.parsers[0]) : [];
1173
+ return def.parsers.flatMap(deriveExpected2);
1174
+ // Union through the NULLABLE prefix, mirroring `sequenceFirstSet`. A leading
1175
+ // `optional(…)`/`many(…)` can match nothing, so the term after it is equally
1176
+ // able to fail first — deriving from term 0 alone named a token the parse
1177
+ // never actually required. A leading `not(…)` is zero-width and contributes
1178
+ // nothing (its own expectation is about what must NOT be here), but it is
1179
+ // nullable, so keep scanning past it. Stop at the first term that must match.
1180
+ case "sequence": {
1181
+ const out = [];
1182
+ for (const term of def.parsers) {
1183
+ if (!isZeroWidthAssertion(term)) out.push(...deriveExpected2(term));
1184
+ if (!matchesEmpty(term)) break;
1185
+ }
1186
+ return out;
1187
+ }
1083
1188
  case "attempt":
1084
- return deriveExpected(def.parser);
1189
+ return deriveExpected2(def.parser);
1085
1190
  // Delegating wrappers whose first-token failure is their inner parser's — must
1086
1191
  // pass through so a start-failure (and the first-set-miss fast-path in attempt/
1087
1192
  // node, which reads this) names the real expected token, not the wrapper tag.
1088
1193
  case "field":
1089
1194
  case "withCtx":
1090
1195
  case "recover":
1091
- return deriveExpected(def.parser);
1196
+ return deriveExpected2(def.parser);
1092
1197
  case "skip":
1093
- return deriveExpected(def.main);
1198
+ return deriveExpected2(def.main);
1094
1199
  case "expect":
1095
1200
  return def.expected;
1096
1201
  case "node":
@@ -1103,13 +1208,18 @@ function deriveExpected(c) {
1103
1208
  case "oneOrMore":
1104
1209
  case "transform":
1105
1210
  case "not":
1106
- return deriveExpected(def.parser);
1211
+ case "peek":
1212
+ return deriveExpected2(def.parser);
1107
1213
  case "lazy": {
1108
1214
  const name = c._ruleName;
1215
+ if (seen.has(c)) return name ? [name] : [];
1216
+ seen.add(c);
1109
1217
  try {
1110
- return deriveExpected(def.thunk());
1218
+ return deriveExpected2(def.thunk());
1111
1219
  } catch {
1112
1220
  return name ? [name] : [];
1221
+ } finally {
1222
+ seen.delete(c);
1113
1223
  }
1114
1224
  }
1115
1225
  default:
@@ -1240,14 +1350,14 @@ function choice(...args) {
1240
1350
  const result = parsers[i].parse(input, pos, ctx);
1241
1351
  if (!result.ok) {
1242
1352
  rollbackCstCapture(ctx, mark);
1243
- if (logLen !== void 0 && ctx._triviaLog) ctx._triviaLog.length = logLen;
1353
+ if (logLen !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== logLen) ctx._triviaLog.length = logLen;
1244
1354
  expected.push(...result.expected);
1245
1355
  continue;
1246
1356
  }
1247
1357
  const checks = autoNot[i];
1248
1358
  if (checks && autoNotFires(input, result.span.end, checks)) {
1249
1359
  rollbackCstCapture(ctx, mark);
1250
- if (logLen !== void 0 && ctx._triviaLog) ctx._triviaLog.length = logLen;
1360
+ if (logLen !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== logLen) ctx._triviaLog.length = logLen;
1251
1361
  continue;
1252
1362
  }
1253
1363
  return result;
@@ -1574,7 +1684,14 @@ function saveTriviaMark(ctx) {
1574
1684
  }
1575
1685
  function rollbackTrivia(ctx, mark) {
1576
1686
  rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors });
1577
- if (ctx._triviaLog) ctx._triviaLog.length = mark.log;
1687
+ if (ctx._triviaLog && ctx._triviaLog.length !== mark.log) ctx._triviaLog.length = mark.log;
1688
+ }
1689
+ function saveLookaheadMark(ctx) {
1690
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null };
1691
+ }
1692
+ function rollbackLookahead(ctx, mark) {
1693
+ rollbackTrivia(ctx, mark);
1694
+ if (ctx._probe) ctx._probe.best = mark.probeBest;
1578
1695
  }
1579
1696
  function scanWithLabels(input, cur, ctx) {
1580
1697
  const triviaP = ctx.trivia;
@@ -1935,7 +2052,7 @@ function attempt(parser2) {
1935
2052
  const result = parser2.parse(input, pos, ctx);
1936
2053
  if (result.ok) return result;
1937
2054
  rollbackCstCapture(ctx, mark);
1938
- if (log !== void 0 && ctx._triviaLog) ctx._triviaLog.length = log;
2055
+ if (log !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== log) ctx._triviaLog.length = log;
1939
2056
  return { ok: false, expected: result.expected, span: { start: pos, end: pos } };
1940
2057
  }
1941
2058
  };
@@ -1973,13 +2090,26 @@ function repItem(combinator, input, cur, ctx, guardable) {
1973
2090
  }
1974
2091
  return { value: result.value, end: result.span.end };
1975
2092
  }
1976
- function many(combinator) {
2093
+ function resolveBounds(what, opts) {
2094
+ const min = opts.min ?? 0;
2095
+ const max = opts.max ?? Infinity;
2096
+ const bad = (msg) => {
2097
+ throw new RangeError(`parseman: ${what} ${msg}`);
2098
+ };
2099
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`);
2100
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`);
2101
+ if (max < min) bad(`max (${max}) is less than min (${min}) \u2014 the combinator could never succeed`);
2102
+ return { min, max };
2103
+ }
2104
+ function many(combinator, opts = {}) {
2105
+ const { min, max } = resolveBounds("many()", opts);
2106
+ if (min >= 1) return atLeast(combinator, min, max);
1977
2107
  const meta = {
1978
2108
  firstSet: combinator._meta.firstSet,
1979
2109
  canMatchNewline: combinator._meta.canMatchNewline,
1980
2110
  isTrivia: false
1981
2111
  };
1982
- const def = { tag: "many", parser: combinator, min: 0 };
2112
+ const def = { tag: "many", parser: combinator, min: 0, ...max === Infinity ? {} : { max } };
1983
2113
  let expected;
1984
2114
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1985
2115
  return {
@@ -1989,7 +2119,9 @@ function many(combinator) {
1989
2119
  parse(input, pos, ctx) {
1990
2120
  const values = def.valueUnused ? void 0 : [];
1991
2121
  let cur = pos;
2122
+ let count = 0;
1992
2123
  while (cur < input.length) {
2124
+ if (count >= max) break;
1993
2125
  const item = repItem(combinator, input, cur, ctx, guardable);
1994
2126
  if (item === "stop") break;
1995
2127
  if ("fail" in item) {
@@ -2000,23 +2132,28 @@ function many(combinator) {
2000
2132
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
2001
2133
  if (values !== void 0) values.push(error);
2002
2134
  captureError(ctx, error);
2135
+ count++;
2003
2136
  cur = end;
2004
2137
  continue;
2005
2138
  }
2006
2139
  if (values !== void 0) values.push(item.value);
2140
+ count++;
2007
2141
  cur = item.end;
2008
2142
  }
2009
2143
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
2010
2144
  }
2011
2145
  };
2012
2146
  }
2013
- function oneOrMore(combinator) {
2147
+ function oneOrMore(combinator, opts = {}) {
2148
+ return many(combinator, { ...opts, min: opts.min ?? 1 });
2149
+ }
2150
+ function atLeast(combinator, min, max) {
2014
2151
  const meta = {
2015
2152
  firstSet: combinator._meta.firstSet,
2016
2153
  canMatchNewline: combinator._meta.canMatchNewline,
2017
2154
  isTrivia: false
2018
2155
  };
2019
- const def = { tag: "oneOrMore", parser: combinator, min: 1 };
2156
+ const def = { tag: "oneOrMore", parser: combinator, min, ...max === Infinity ? {} : { max } };
2020
2157
  let expected;
2021
2158
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2022
2159
  return {
@@ -2028,7 +2165,19 @@ function oneOrMore(combinator) {
2028
2165
  if (!first.ok) return first;
2029
2166
  const values = def.valueUnused ? void 0 : [first.value];
2030
2167
  let cur = first.span.end;
2168
+ let count = 1;
2169
+ while (count < min) {
2170
+ const item = repItem(combinator, input, cur, ctx, guardable);
2171
+ if (item === "stop" || "fail" in item) {
2172
+ expected ??= deriveExpected(combinator);
2173
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
2174
+ }
2175
+ if (values !== void 0) values.push(item.value);
2176
+ cur = item.end;
2177
+ count++;
2178
+ }
2031
2179
  while (cur < input.length) {
2180
+ if (count >= max) break;
2032
2181
  const item = repItem(combinator, input, cur, ctx, guardable);
2033
2182
  if (item === "stop") break;
2034
2183
  if ("fail" in item) {
@@ -2039,10 +2188,12 @@ function oneOrMore(combinator) {
2039
2188
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
2040
2189
  if (values !== void 0) values.push(error);
2041
2190
  captureError(ctx, error);
2191
+ count++;
2042
2192
  cur = end;
2043
2193
  continue;
2044
2194
  }
2045
2195
  if (values !== void 0) values.push(item.value);
2196
+ count++;
2046
2197
  cur = item.end;
2047
2198
  }
2048
2199
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
@@ -2072,17 +2223,33 @@ function optional(combinator) {
2072
2223
  }
2073
2224
  };
2074
2225
  }
2075
- function sepBy(combinator, separator) {
2226
+ function oneOrMoreSep(combinator, separator, opts = {}) {
2227
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 });
2228
+ }
2229
+ function sepBy(combinator, separator, opts = {}) {
2230
+ const { min, max } = resolveBounds("sepBy()", opts);
2231
+ const trailing = opts.trailing ?? "forbid";
2076
2232
  const meta = {
2077
2233
  firstSet: combinator._meta.firstSet,
2078
2234
  canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
2079
2235
  isTrivia: false
2080
2236
  };
2081
2237
  let expected;
2238
+ const failAt2 = (at) => {
2239
+ expected ??= deriveExpected(combinator);
2240
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } };
2241
+ };
2082
2242
  return {
2083
2243
  _tag: "sepBy",
2084
2244
  _meta: meta,
2085
- _def: { tag: "sepBy", parser: combinator, separator },
2245
+ _def: {
2246
+ tag: "sepBy",
2247
+ parser: combinator,
2248
+ separator,
2249
+ min,
2250
+ ...max === Infinity ? {} : { max },
2251
+ ...trailing === "forbid" ? {} : { trailing }
2252
+ },
2086
2253
  parse(input, pos, ctx) {
2087
2254
  const first = combinator.parse(input, pos, ctx);
2088
2255
  const values = [];
@@ -2093,6 +2260,7 @@ function sepBy(combinator, separator) {
2093
2260
  } else {
2094
2261
  const term = ctx._tolerant ? ctx._sync : void 0;
2095
2262
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2263
+ if (min >= 1) return failAt2(pos);
2096
2264
  return { ok: true, value: [], span: { start: pos, end: pos } };
2097
2265
  }
2098
2266
  expected ??= deriveExpected(combinator);
@@ -2102,6 +2270,7 @@ function sepBy(combinator, separator) {
2102
2270
  cur = rec.end;
2103
2271
  }
2104
2272
  while (cur < input.length) {
2273
+ if (values.length >= max) break;
2105
2274
  const loopMark = saveTriviaMark(ctx);
2106
2275
  let sepPos = cur;
2107
2276
  if (ctx.trivia) {
@@ -2118,6 +2287,7 @@ function sepBy(combinator, separator) {
2118
2287
  rollbackTrivia(ctx, loopMark);
2119
2288
  break;
2120
2289
  }
2290
+ const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
2121
2291
  let nextPos = sep.span.end;
2122
2292
  if (ctx.trivia) {
2123
2293
  if (needsDeferredTriviaCommit(ctx)) {
@@ -2139,12 +2309,18 @@ function sepBy(combinator, separator) {
2139
2309
  cur = rec.end;
2140
2310
  continue;
2141
2311
  }
2312
+ if (sepMark !== void 0) {
2313
+ rollbackTrivia(ctx, sepMark);
2314
+ cur = sep.span.end;
2315
+ break;
2316
+ }
2142
2317
  rollbackTrivia(ctx, loopMark);
2143
2318
  break;
2144
2319
  }
2145
2320
  values.push(next.value);
2146
2321
  cur = next.span.end;
2147
2322
  }
2323
+ if (values.length < min) return failAt2(cur);
2148
2324
  return { ok: true, value: values, span: { start: pos, end: cur } };
2149
2325
  }
2150
2326
  };
@@ -2234,6 +2410,7 @@ function markUnusedValues(root) {
2234
2410
  visit(def.separator, false);
2235
2411
  return;
2236
2412
  case "not":
2413
+ case "peek":
2237
2414
  visit(def.parser, false);
2238
2415
  return;
2239
2416
  case "grammar":
@@ -2352,9 +2529,9 @@ function not(combinator) {
2352
2529
  _meta: meta,
2353
2530
  _def: { tag: "not", parser: combinator },
2354
2531
  parse(input, pos, ctx) {
2355
- const mark = saveCstMark(ctx);
2532
+ const mark = saveLookaheadMark(ctx);
2356
2533
  const result = combinator.parse(input, pos, ctx);
2357
- rollbackCstCapture(ctx, mark);
2534
+ rollbackLookahead(ctx, mark);
2358
2535
  if (result.ok) {
2359
2536
  return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } };
2360
2537
  }
@@ -2363,6 +2540,28 @@ function not(combinator) {
2363
2540
  };
2364
2541
  }
2365
2542
 
2543
+ // src/combinators/peek.ts
2544
+ function peek(combinator) {
2545
+ const meta = {
2546
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
2547
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
2548
+ canMatchNewline: false,
2549
+ isTrivia: false
2550
+ };
2551
+ return {
2552
+ _tag: "peek",
2553
+ _meta: meta,
2554
+ _def: { tag: "peek", parser: combinator },
2555
+ parse(input, pos, ctx) {
2556
+ const mark = saveLookaheadMark(ctx);
2557
+ const result = combinator.parse(input, pos, ctx);
2558
+ rollbackLookahead(ctx, mark);
2559
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } };
2560
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } };
2561
+ }
2562
+ };
2563
+ }
2564
+
2366
2565
  // src/compiler/build-arity.ts
2367
2566
  var PARAM_LIST_RE = (
2368
2567
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
@@ -2461,6 +2660,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2461
2660
  case "expect":
2462
2661
  case "withCtx":
2463
2662
  case "not":
2663
+ case "peek":
2464
2664
  return parserHasOwnFields(d.parser, seen);
2465
2665
  default:
2466
2666
  return false;
@@ -2500,6 +2700,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2500
2700
  case "expect":
2501
2701
  case "withCtx":
2502
2702
  case "not":
2703
+ case "peek":
2503
2704
  case "field":
2504
2705
  return parserHasTriviaSite(d.parser, seen);
2505
2706
  case "choice":
@@ -2549,6 +2750,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2549
2750
  case "expect":
2550
2751
  case "withCtx":
2551
2752
  case "not":
2753
+ case "peek":
2552
2754
  return parserEnablesTriviaCapture(d.parser, seen);
2553
2755
  // leaf() clears the inner CST/trivia collector and publishes only its own
2554
2756
  // terminal result. An inner parser() capture cannot reach this node.
@@ -2924,7 +3126,7 @@ function leaf(root, fn) {
2924
3126
  }
2925
3127
 
2926
3128
  // src/version.ts
2927
- var PARSEMAN_VERSION = "0.33.0";
3129
+ var PARSEMAN_VERSION = "0.35.0";
2928
3130
 
2929
3131
  // src/analysis/gating.ts
2930
3132
  var isAny = (fs) => fs.kind === "any";
@@ -2964,14 +3166,15 @@ function firstSetToString(fs) {
2964
3166
  var ruleNameOf = (p) => p._ruleName;
2965
3167
  var SUGGESTIONS = {
2966
3168
  "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.",
3169
+ "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.",
3170
+ "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
3171
  "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
3172
  "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2971
3173
  "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2972
3174
  };
2973
- function classifyBroadArm(arm) {
3175
+ function classifyBroadArm(arm, resolve) {
2974
3176
  const seen = /* @__PURE__ */ new Set();
3177
+ const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
2975
3178
  const walk2 = (p) => {
2976
3179
  if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2977
3180
  seen.add(p);
@@ -2983,6 +3186,10 @@ function classifyBroadArm(arm) {
2983
3186
  return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2984
3187
  case "not":
2985
3188
  return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
3189
+ // peek() CARRIES its body's first-set, so it is only broad when the body is
3190
+ // (or the body is nullable) — walk in for the real cause.
3191
+ case "peek":
3192
+ return walk2(d.parser);
2986
3193
  case "scanTo":
2987
3194
  return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2988
3195
  // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
@@ -2999,18 +3206,23 @@ function classifyBroadArm(arm) {
2999
3206
  return walk2(d.parser);
3000
3207
  case "lazy": {
3001
3208
  const name = p._ruleName;
3209
+ let target;
3002
3210
  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;
3211
+ target = d.thunk();
3006
3212
  } catch {
3007
- return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3213
+ const bound = name !== void 0 ? resolve?.(name) : void 0;
3214
+ if (bound === void 0)
3215
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}`, unresolvedExternal: name !== void 0 };
3216
+ target = bound;
3008
3217
  }
3218
+ const inner = walk2(target);
3219
+ if (name !== void 0) return inner ? relabel(inner, "cross-artifact-ref", `via ref g.${name} \u2192 ${inner.detail}`) : null;
3220
+ return inner;
3009
3221
  }
3010
3222
  case "optional":
3011
3223
  case "many": {
3012
3224
  const inner = walk2(d.parser);
3013
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3225
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
3014
3226
  }
3015
3227
  case "oneOrMore":
3016
3228
  case "transform":
@@ -3032,13 +3244,13 @@ function classifyBroadArm(arm) {
3032
3244
  sawNullablePrefix = true;
3033
3245
  continue;
3034
3246
  }
3035
- if (isAny(firstSetOf(t))) {
3247
+ if (isAny(firstSetOf(t, /* @__PURE__ */ new Set(), resolve))) {
3036
3248
  const inner = walk2(t);
3037
3249
  if (sawNullablePrefix)
3038
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3250
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
3039
3251
  return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
3040
3252
  }
3041
- if (matchesEmpty(t)) {
3253
+ if (matchesEmpty(t, /* @__PURE__ */ new Set(), resolve)) {
3042
3254
  sawNullablePrefix = true;
3043
3255
  continue;
3044
3256
  }
@@ -3049,7 +3261,7 @@ function classifyBroadArm(arm) {
3049
3261
  case "choice": {
3050
3262
  for (const a of d.parsers) {
3051
3263
  const r = walk2(a);
3052
- if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3264
+ if (r) return relabel(r, r.cause, `choice arm \u2192 ${r.detail}`);
3053
3265
  }
3054
3266
  return null;
3055
3267
  }
@@ -3086,7 +3298,7 @@ function detectAntiPatterns(rule, arms) {
3086
3298
  kind: "double-not",
3087
3299
  rule,
3088
3300
  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."
3301
+ 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
3302
  });
3091
3303
  } else {
3092
3304
  out.push({
@@ -3109,6 +3321,9 @@ function detectAntiPatterns(rule, arms) {
3109
3321
  return out;
3110
3322
  }
3111
3323
  function analyzeGating(entry, opts) {
3324
+ return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3325
+ }
3326
+ function analyzeGatingRules(ruleMap, opts) {
3112
3327
  const raw = [];
3113
3328
  const antiPatterns = [];
3114
3329
  const visited = /* @__PURE__ */ new Set();
@@ -3118,7 +3333,7 @@ function analyzeGating(entry, opts) {
3118
3333
  const d = p._def;
3119
3334
  const rule = ruleNameOf(p) ?? enclosingRule;
3120
3335
  if (d.tag === "choice") {
3121
- raw.push({ g: analyzeChoice(p, d, rule), rule });
3336
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
3122
3337
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3123
3338
  }
3124
3339
  const rec = d;
@@ -3135,7 +3350,7 @@ function analyzeGating(entry, opts) {
3135
3350
  }
3136
3351
  for (const k of kids) visit(k, rule);
3137
3352
  };
3138
- visit(entry, ruleNameOf(entry) ?? "<entry>");
3353
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3139
3354
  const perRule = /* @__PURE__ */ new Map();
3140
3355
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3141
3356
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3151,18 +3366,19 @@ function analyzeGating(entry, opts) {
3151
3366
  });
3152
3367
  const gated = choices.filter((c) => c.gates === "yes").length;
3153
3368
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3154
- const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3369
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted && !c.deferred);
3155
3370
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3371
+ const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3156
3372
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3157
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3373
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3158
3374
  }
3159
- function analyzeChoice(p, d, rule) {
3375
+ function analyzeChoice(p, d, rule, resolve) {
3160
3376
  const arms = d.parsers;
3161
3377
  const shallow = arms.map((a) => a._meta.firstSet);
3162
- const deep = arms.map((a) => firstSetOf(a));
3378
+ const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
3163
3379
  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] };
3380
+ const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
3381
+ return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
3166
3382
  });
3167
3383
  const overlaps = [];
3168
3384
  for (let i = 0; i < deep.length; i++)
@@ -3178,10 +3394,12 @@ function analyzeChoice(p, d, rule) {
3178
3394
  if (d.disjoint) gates = "yes";
3179
3395
  else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3180
3396
  else gates = "no";
3397
+ const deferred = gates === "no" && overlaps.length === 0 && anyArms.length > 0 && anyArms.every((a) => a.unresolvedExternal);
3181
3398
  return {
3182
3399
  rule,
3183
3400
  strategy: d.strategy?.tag ?? "firstMatch",
3184
3401
  gates,
3402
+ deferred,
3185
3403
  combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3186
3404
  anyArms,
3187
3405
  overlaps
@@ -3348,6 +3566,7 @@ function children(def, winners) {
3348
3566
  case "field":
3349
3567
  case "grammar":
3350
3568
  case "not":
3569
+ case "peek":
3351
3570
  case "node":
3352
3571
  case "guard":
3353
3572
  case "withCtx":
@@ -5098,9 +5317,9 @@ function planDisjointDispatch(parsers) {
5098
5317
  return total >= SWITCH_MIN_CASES ? { kind: "switch", cases } : { kind: "if" };
5099
5318
  }
5100
5319
  function captureRestoreBody(mL, mR, mTl, mLg, mF = null) {
5101
- const fieldRestore = mF ? `; if (_ctx._fields) _ctx._fields.length = ${mF}` : "";
5102
- const base = `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${mL}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${mR}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${mTl}${fieldRestore}`;
5103
- return mLg ? `${base}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${mLg}` : base;
5320
+ const fieldRestore = mF ? `; if (_ctx._fields && _ctx._fields.length !== ${mF}) _ctx._fields.length = ${mF}` : "";
5321
+ const base = `if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${mL}) _ctx._cstLeaves.length = ${mL}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${mR}) _ctx._cstRawChildren.length = ${mR}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${mTl}) _ctx._cstTriviaLog.length = ${mTl}${fieldRestore}`;
5322
+ return mLg ? `${base}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${mLg}) _ctx._triviaLog.length = ${mLg}` : base;
5104
5323
  }
5105
5324
  function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5106
5325
  if (seen.has(p)) return false;
@@ -5113,6 +5332,9 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5113
5332
  case "keywords":
5114
5333
  case "guard":
5115
5334
  case "not":
5335
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
5336
+ // outcomes, so it can never leave a partial capture behind.
5337
+ case "peek":
5116
5338
  case "trivia":
5117
5339
  case "token":
5118
5340
  case "leaf":
@@ -5184,6 +5406,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
5184
5406
  case "leaf":
5185
5407
  return true;
5186
5408
  case "not":
5409
+ case "peek":
5187
5410
  case "guard":
5188
5411
  case "trivia":
5189
5412
  case "unknown":
@@ -5310,6 +5533,7 @@ function emitKeywordsFast(def, ctx, pos) {
5310
5533
  if (def.words.length === 0 || def.words.some((w) => w.length === 0)) return null;
5311
5534
  if (def.words.some((w) => Array.from(w).length !== w.length)) return null;
5312
5535
  if (def.caseInsensitive && def.boundary) return null;
5536
+ if (def.caseInsensitive && def.words.some((w) => w.split("").some((c) => c.charCodeAt(0) > 127))) return null;
5313
5537
  let boundary = null;
5314
5538
  if (def.boundary) {
5315
5539
  let body = def.boundary;
@@ -5357,7 +5581,7 @@ function emitKeywords(def, ctx, pos) {
5357
5581
  if (fast) return fast;
5358
5582
  const alt = def.words.map(escapeKeywordRe).join("|");
5359
5583
  const boundary = def.boundary ? `(?![${def.boundary}])` : "";
5360
- const flags = def.caseInsensitive ? "iuy" : "uy";
5584
+ const flags = def.caseInsensitive ? "iy" : "uy";
5361
5585
  const source = `(?:${alt})${boundary}`;
5362
5586
  const key = `${source}/${flags}`;
5363
5587
  let rName = ctx.regexMap.get(key);
@@ -5457,7 +5681,7 @@ function emitSeqValues(def, ctx, pos) {
5457
5681
  const endAfterV = v(ctx, "_sea");
5458
5682
  stmts.push(
5459
5683
  `${ind(ctx)}const ${endAfterV} = ${r2.endVar}`,
5460
- `${ind(ctx)}if (${endAfterV} > ${scanEndV}) { ${curV} = ${endAfterV} } else { if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${markLog}; }`
5684
+ `${ind(ctx)}if (${endAfterV} > ${scanEndV}) { ${curV} = ${endAfterV} } else { if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${markV}) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${markTl}) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${markLog}) _ctx._triviaLog.length = ${markLog}; }`
5461
5685
  );
5462
5686
  valueVars.push(r2.valueVar);
5463
5687
  continue;
@@ -5503,6 +5727,7 @@ function failsAtStart(p) {
5503
5727
  case "keywords":
5504
5728
  case "guard":
5505
5729
  case "not":
5730
+ case "peek":
5506
5731
  return true;
5507
5732
  case "transform":
5508
5733
  case "label":
@@ -5916,14 +6141,17 @@ function emitMany(def, ctx, pos) {
5916
6141
  ];
5917
6142
  const mySyncV = ctx.recovery ? v(ctx, "_mysy") : "";
5918
6143
  if (ctx.recovery) stmts.push(`${ind(ctx)}const ${mySyncV} = _ctx._sync`);
5919
- if (def.min === 1) {
6144
+ for (let i = 0; i < def.min; i++) {
5920
6145
  const firstR = emit(def.parser, ctx, curV);
5921
6146
  stmts.push(...firstR.stmts);
5922
6147
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
5923
6148
  stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
5924
6149
  }
6150
+ const maxV = def.max === void 0 ? null : wantValue ? `${arrV}.length` : v(ctx, "_cnt");
6151
+ if (maxV !== null && !wantValue) stmts.push(`${ind(ctx)}let ${maxV} = ${def.min}`);
5925
6152
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
5926
6153
  ctx.indent++;
6154
+ if (maxV !== null) stmts.push(`${ind(ctx)}if (${maxV} >= ${def.max}) break`);
5927
6155
  let itemPos = curV;
5928
6156
  let rollback = "";
5929
6157
  if (ctx.activeTrivia) {
@@ -5940,7 +6168,7 @@ function emitMany(def, ctx, pos) {
5940
6168
  `${ind(ctx)}const ${npV} = ${capFn}(input, ${curV}, _ctx, 1)`
5941
6169
  );
5942
6170
  itemPos = npV;
5943
- rollback = `if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${markLog}; `;
6171
+ rollback = `if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${markV}) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${markTl}) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${markLog}) _ctx._triviaLog.length = ${markLog}; `;
5944
6172
  } else {
5945
6173
  const trivFn = ensureTriviaFn(ctx);
5946
6174
  const npV = v(ctx, "_np");
@@ -5964,7 +6192,7 @@ function emitMany(def, ctx, pos) {
5964
6192
  `${ind(ctx)}if (!${iterOk}) {`,
5965
6193
  `${ind(ctx)} ${rollback}if (_ctx._tolerant && ${mySyncV} !== undefined && !_ctx._rec.at(${mySyncV}, input, ${itemPos}, _ctx)) {`,
5966
6194
  `${ind(ctx)} const ${rrV} = _ctx._rec.scan(input, ${itemPos}, _ctx, ${mySyncV}, ${exp})`,
5967
- ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : [],
6195
+ ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : maxV !== null ? [`${ind(ctx)} ${maxV}++`] : [],
5968
6196
  `${ind(ctx)} _ctx._rec.capture(_ctx, ${rrV}.error)`,
5969
6197
  `${ind(ctx)} ${curV} = ${rrV}.end`,
5970
6198
  `${ind(ctx)} continue`,
@@ -5977,6 +6205,7 @@ function emitMany(def, ctx, pos) {
5977
6205
  stmts.push(`${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`);
5978
6206
  }
5979
6207
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
6208
+ else if (maxV !== null) stmts.push(`${ind(ctx)}${maxV}++`);
5980
6209
  stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
5981
6210
  ctx.indent--;
5982
6211
  stmts.push(`${ind(ctx)}}`);
@@ -6002,7 +6231,7 @@ function emitAttempt(p, def, ctx, pos) {
6002
6231
  const log = v(ctx, "_atg");
6003
6232
  const fields = v(ctx, "_atf");
6004
6233
  const errors = v(ctx, "_ate");
6005
- const rollback = `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${leaves}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${raw}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${trivia2}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${log}; if (_ctx._fields) _ctx._fields.length = ${fields}; if (_ctx._errors) _ctx._errors.length = ${errors}`;
6234
+ const rollback = `if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${leaves}) _ctx._cstLeaves.length = ${leaves}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${raw}) _ctx._cstRawChildren.length = ${raw}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${trivia2}) _ctx._cstTriviaLog.length = ${trivia2}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${log}) _ctx._triviaLog.length = ${log}; if (_ctx._fields && _ctx._fields.length !== ${fields}) _ctx._fields.length = ${fields}; if (_ctx._errors && _ctx._errors.length !== ${errors}) _ctx._errors.length = ${errors}`;
6006
6235
  const traceId = ctx.coverage?.plan.attempts.get(p);
6007
6236
  const traceRollback = traceId === void 0 ? "" : ` _ctx._grammarTrace?.write({ id: ${JSON.stringify(traceId)}, phase: 'rollback', offset: ${pos} });`;
6008
6237
  const preGuard = [];
@@ -6041,8 +6270,8 @@ function emitSepBy(_p, def, ctx, pos) {
6041
6270
  ...rec ? [`${ind(ctx)}const ${mySyncV} = _ctx._sync`] : [],
6042
6271
  ...firstStmts
6043
6272
  ];
6044
- const failItem = (nextOkVar, itemPosVar, rb, originalFail) => {
6045
- if (!rec) return [originalFail];
6273
+ const failItem = (nextOkVar, itemPosVar, breakStmt) => {
6274
+ if (!rec) return [`${ind(ctx)}if (!${nextOkVar}) ${breakStmt === "break" ? "break" : `{ ${breakStmt} }`}`];
6046
6275
  const rr = v(ctx, "_rr");
6047
6276
  return [
6048
6277
  `${ind(ctx)}if (!${nextOkVar}) {`,
@@ -6050,10 +6279,24 @@ function emitSepBy(_p, def, ctx, pos) {
6050
6279
  `${ind(ctx)} const ${rr} = _ctx._rec.scan(input, ${itemPosVar}, _ctx, ${scanSync}, ${exp})`,
6051
6280
  `${ind(ctx)} ${arrV}.push(${rr}.error); _ctx._rec.capture(_ctx, ${rr}.error); ${curV} = ${rr}.end; continue`,
6052
6281
  `${ind(ctx)} }`,
6053
- `${ind(ctx)} ${rb}break`,
6282
+ `${ind(ctx)} ${breakStmt}`,
6054
6283
  `${ind(ctx)}}`
6055
6284
  ];
6056
6285
  };
6286
+ const itemFailBreak = (sepEndVar, sepRb, postSepRb) => def.trailing === void 0 ? `${sepRb}break` : `${postSepRb}${curV} = ${sepEndVar}; break`;
6287
+ const postSepMarks = () => {
6288
+ if (def.trailing === void 0 || !ctx.capturing) return { decl: [], rb: "" };
6289
+ const lv = v(ctx, "_tlv"), rw = v(ctx, "_trw"), tl = v(ctx, "_ttl"), lg = v(ctx, "_tlg");
6290
+ return {
6291
+ decl: [
6292
+ `${ind(ctx)}const ${lv} = _ctx._cstLeaves ? _ctx._cstLeaves.length : 0`,
6293
+ `${ind(ctx)}const ${rw} = _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0`,
6294
+ `${ind(ctx)}const ${tl} = _ctx._cstTriviaLog ? _ctx._cstTriviaLog.length : 0`,
6295
+ `${ind(ctx)}const ${lg} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`
6296
+ ],
6297
+ rb: `if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${lv}) _ctx._cstLeaves.length = ${lv}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${rw}) _ctx._cstRawChildren.length = ${rw}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${tl}) _ctx._cstTriviaLog.length = ${tl}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${lg}) _ctx._triviaLog.length = ${lg}; `
6298
+ };
6299
+ };
6057
6300
  if (rec) {
6058
6301
  const rr0 = v(ctx, "_rr0");
6059
6302
  const didV = v(ctx, "_did");
@@ -6079,6 +6322,7 @@ function emitSepBy(_p, def, ctx, pos) {
6079
6322
  );
6080
6323
  ctx.indent++;
6081
6324
  }
6325
+ if (def.max !== void 0) stmts.push(`${ind(ctx)}if (${arrV}.length >= ${def.max}) break`);
6082
6326
  let sepAtPos = curV;
6083
6327
  if (ctx.activeTrivia) {
6084
6328
  if (ctx.capturing) {
@@ -6096,15 +6340,17 @@ function emitSepBy(_p, def, ctx, pos) {
6096
6340
  `${ind(ctx)}const ${spV} = ${capFn}(input, ${curV}, _ctx, 1)`
6097
6341
  );
6098
6342
  sepAtPos = spV;
6099
- 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}; `;
6343
+ const rollbackToSep = `if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${markLv}) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${markV}) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${markTl}) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${markLog}) _ctx._triviaLog.length = ${markLog}; `;
6100
6344
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6101
6345
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) { ${rollbackToSep}break }`);
6346
+ const post = postSepMarks();
6347
+ stmts.push(...post.decl);
6102
6348
  const npV = v(ctx, "_np");
6103
6349
  stmts.push(`${ind(ctx)}const ${npV} = ${capFn}(input, ${sepEnd}, _ctx, 1)`);
6104
6350
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6105
6351
  stmts.push(
6106
6352
  ...nextStmts,
6107
- ...failItem(nextOk, npV, rollbackToSep, `${ind(ctx)}if (!${nextOk}) { ${rollbackToSep}break }`),
6353
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, rollbackToSep, post.rb)),
6108
6354
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6109
6355
  `${ind(ctx)}${curV} = ${nextEnd}`
6110
6356
  );
@@ -6120,7 +6366,7 @@ function emitSepBy(_p, def, ctx, pos) {
6120
6366
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6121
6367
  stmts.push(
6122
6368
  ...nextStmts,
6123
- ...failItem(nextOk, npV, "", `${ind(ctx)}if (!${nextOk}) break`),
6369
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, "", "")),
6124
6370
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6125
6371
  `${ind(ctx)}${curV} = ${nextEnd}`
6126
6372
  );
@@ -6136,11 +6382,13 @@ function emitSepBy(_p, def, ctx, pos) {
6136
6382
  }
6137
6383
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6138
6384
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) break`);
6139
- const nextRb = markLv ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6385
+ const nextRb = markLv ? `if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${markLv}) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${markRw}) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6386
+ const post = postSepMarks();
6387
+ stmts.push(...post.decl);
6140
6388
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, sepEnd, true);
6141
6389
  stmts.push(
6142
6390
  ...nextStmts,
6143
- ...failItem(nextOk, sepEnd, nextRb, `${ind(ctx)}if (!${nextOk}) { ${nextRb}break }`),
6391
+ ...failItem(nextOk, sepEnd, itemFailBreak(sepEnd, nextRb, post.rb)),
6144
6392
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6145
6393
  `${ind(ctx)}${curV} = ${nextEnd}`
6146
6394
  );
@@ -6149,6 +6397,9 @@ function emitSepBy(_p, def, ctx, pos) {
6149
6397
  stmts.push(`${ind(ctx)}}`);
6150
6398
  ctx.indent--;
6151
6399
  stmts.push(`${ind(ctx)}}`);
6400
+ if (def.min >= 1) {
6401
+ stmts.push(...emitIfFail(ctx, `${arrV}.length < ${def.min}`, failArrBody(ctx, deriveExpectedArr([def.parser]), curV)));
6402
+ }
6152
6403
  return { stmts, valueVar: arrV, endVar: curV };
6153
6404
  }
6154
6405
  function emitScanTo(def, ctx, pos) {
@@ -6200,17 +6451,44 @@ function emitScanTo(def, ctx, pos) {
6200
6451
  return { stmts, valueVar: valV, endVar: curV };
6201
6452
  }
6202
6453
  function emitNot(def, ctx, pos) {
6454
+ const sinksLive = !!ctx.capturing || !!ctx.recovery;
6455
+ const leaves = sinksLive ? v(ctx, "_ntl") : null;
6456
+ const raw = sinksLive ? v(ctx, "_ntr") : null;
6457
+ const tl = sinksLive ? v(ctx, "_ntt") : null;
6458
+ const log = sinksLive ? v(ctx, "_ntg") : null;
6459
+ const fields = sinksLive ? v(ctx, "_ntf") : null;
6460
+ const errors = sinksLive ? v(ctx, "_nte") : null;
6203
6461
  const { stmts, okVar } = emitFallible(def.parser, ctx, pos, true);
6204
6462
  const label2 = JSON.stringify(`not(${def.parser._tag})`);
6205
6463
  return {
6206
6464
  stmts: [
6465
+ ...sinksLive ? [
6466
+ `${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`
6467
+ ] : [],
6207
6468
  ...stmts,
6469
+ ...sinksLive ? [
6470
+ `${ind(ctx)}if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${leaves}) _ctx._cstLeaves.length = ${leaves}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${raw}) _ctx._cstRawChildren.length = ${raw}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${tl}) _ctx._cstTriviaLog.length = ${tl}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${log}) _ctx._triviaLog.length = ${log}; if (_ctx._fields && _ctx._fields.length !== ${fields}) _ctx._fields.length = ${fields}; if (_ctx._errors && _ctx._errors.length !== ${errors}) _ctx._errors.length = ${errors}`
6471
+ ] : [],
6208
6472
  ...emitIfFail(ctx, okVar, failBody(ctx, label2, pos))
6209
6473
  ],
6210
6474
  valueVar: "null",
6211
6475
  endVar: pos
6212
6476
  };
6213
6477
  }
6478
+ function emitPeek(def, ctx, pos) {
6479
+ const probeCtx = { ...ctx, capturing: false, noHoist: true };
6480
+ const { stmts, okVar } = emitFallible(def.parser, probeCtx, pos, true);
6481
+ ctx.vars = probeCtx.vars;
6482
+ const label2 = JSON.stringify(`peek(${def.parser._tag})`);
6483
+ return {
6484
+ stmts: [
6485
+ ...stmts,
6486
+ ...emitIfFail(ctx, `!${okVar}`, failBody(ctx, label2, pos))
6487
+ ],
6488
+ valueVar: "null",
6489
+ endVar: pos
6490
+ };
6491
+ }
6214
6492
  function escapeTokenLiteral(value) {
6215
6493
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6216
6494
  }
@@ -6498,9 +6776,9 @@ function emitRuntimeFallback(parser2, ctx, pos) {
6498
6776
  return { stmts, valueVar: vv, endVar: ev };
6499
6777
  }
6500
6778
  function emitLazy(p, def, ctx, pos) {
6501
- const canonical = ctx.ruleNames?.get(p);
6779
+ const canonical2 = ctx.ruleNames?.get(p);
6502
6780
  const usage = ctx.lazyUsage;
6503
- if (!canonical && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6781
+ if (!canonical2 && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6504
6782
  let resolved;
6505
6783
  try {
6506
6784
  resolved = def.thunk();
@@ -6510,7 +6788,7 @@ function emitLazy(p, def, ctx, pos) {
6510
6788
  return emit(resolved, ctx, pos);
6511
6789
  }
6512
6790
  if (!ctx.namedParsers.has(p)) {
6513
- const fnName2 = canonical ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6791
+ const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6514
6792
  ctx.namedParsers.set(p, fnName2);
6515
6793
  let resolved;
6516
6794
  try {
@@ -6793,6 +7071,8 @@ function emitDispatch(p, ctx, pos) {
6793
7071
  }
6794
7072
  case "not":
6795
7073
  return emitNot(def, ctx, pos);
7074
+ case "peek":
7075
+ return emitPeek(def, ctx, pos);
6796
7076
  case "node":
6797
7077
  return emitNode(def, ctx, pos);
6798
7078
  case "scanTo":
@@ -6881,6 +7161,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
6881
7161
  case "many":
6882
7162
  case "oneOrMore":
6883
7163
  case "not":
7164
+ case "peek":
6884
7165
  case "transform":
6885
7166
  return hasNodeDef(d.parser, seen);
6886
7167
  case "skip":
@@ -6932,17 +7213,18 @@ function childrenOf(def) {
6932
7213
  case "field":
6933
7214
  case "grammar":
6934
7215
  case "not":
7216
+ case "peek":
6935
7217
  case "node":
6936
7218
  case "withCtx":
6937
7219
  case "expect":
6938
7220
  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
7221
+ // emitMany's min>=1 branch and emitSepBy both codegen `def.parser` more than
7222
+ // once (`min` mandatory matches, then again inside the repeat loop) — each is a
7223
+ // real emit() call site, so the usage analysis must see one edge per site or it
6942
7224
  // undercounts a single-use `parser` ref as inline-safe when it's actually
6943
- // referenced from two positions within this one compiled function.
7225
+ // referenced from several positions within this one compiled function.
6944
7226
  case "oneOrMore":
6945
- return [def.parser, def.parser];
7227
+ return Array.from({ length: def.min + 1 }, () => def.parser);
6946
7228
  case "sepBy":
6947
7229
  return [def.parser, def.parser, def.separator];
6948
7230
  case "skip":
@@ -7088,12 +7370,41 @@ function resolveGatingLevel(opt) {
7088
7370
  return "warn";
7089
7371
  }
7090
7372
  function runGatingDiagnostic(combinator, opt) {
7373
+ return reportGating(opt, (o) => analyzeGating(combinator, o));
7374
+ }
7375
+ function runGatingDiagnosticRules(ruleMap, opt) {
7376
+ return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7377
+ }
7378
+ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7379
+ if (resolveGatingLevel(opt) === "off") return void 0;
7380
+ const ruleMaps = getRuleMaps();
7381
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
7382
+ const winners = /* @__PURE__ */ new Map();
7383
+ for (const map of ruleMaps) for (const [name, rule] of map) {
7384
+ if (rule._def.tag === "lazy") {
7385
+ try {
7386
+ rule._def.thunk();
7387
+ } catch {
7388
+ continue;
7389
+ }
7390
+ }
7391
+ winners.set(name, rule);
7392
+ }
7393
+ const entries = [...winners];
7394
+ return reportGating(opt, (o) => {
7395
+ const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7396
+ const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7397
+ return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
7398
+ });
7399
+ }
7400
+ function reportGating(opt, analyze) {
7091
7401
  const level = resolveGatingLevel(opt);
7092
7402
  if (level === "off") return void 0;
7093
- const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7403
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
7404
+ 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
7405
  let report;
7095
7406
  try {
7096
- report = analyzeGating(combinator, accept ? { accept } : void 0);
7407
+ report = analyze(analyzeOpts);
7097
7408
  } catch {
7098
7409
  return void 0;
7099
7410
  }
@@ -7230,6 +7541,7 @@ function publicRuleWrapperSource(rule, fnSource) {
7230
7541
  }
7231
7542
  function compileLinkable(ruleMapArg, ns, opts) {
7232
7543
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7544
+ if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
7233
7545
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7234
7546
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7235
7547
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
@@ -7286,6 +7598,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7286
7598
  fnNameToKey.set(fn, key);
7287
7599
  }
7288
7600
  ctx.ruleNames = ruleNames;
7601
+ const externalRefs = /* @__PURE__ */ new Set();
7289
7602
  {
7290
7603
  const scanned = /* @__PURE__ */ new Set();
7291
7604
  const scanExternal = (p) => {
@@ -7301,11 +7614,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
7301
7614
  }
7302
7615
  if (resolved === void 0) {
7303
7616
  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);
7617
+ if (name) {
7618
+ externalRefs.add(p);
7619
+ if (!ctx.namedParsers.has(p)) {
7620
+ assertRuleName(name);
7621
+ const fn = `_r_${name}`;
7622
+ ruleNames.set(p, fn);
7623
+ ctx.namedParsers.set(p, fn);
7624
+ }
7309
7625
  }
7310
7626
  return;
7311
7627
  }
@@ -7418,12 +7734,32 @@ function compileLinkable(ruleMapArg, ns, opts) {
7418
7734
  needsEmptyTl: !!ctx.needsEmptyTl,
7419
7735
  needsHostReads: !!ctx.needsHostReads,
7420
7736
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7421
- isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule)),
7737
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7422
7738
  mfFns: mfSrcs ? [] : ctx.mapFns,
7423
7739
  buildFns: buildSrcs ? [] : ctx.buildFns
7424
7740
  };
7425
7741
  }
7426
- function hasSemanticReduction(roots) {
7742
+ function hasExternalRuleRef(ruleMap) {
7743
+ const seen = /* @__PURE__ */ new Set();
7744
+ const visit = (p) => {
7745
+ if (seen.has(p)) return false;
7746
+ seen.add(p);
7747
+ const def = p._def;
7748
+ if (def.tag === "lazy") {
7749
+ let resolved;
7750
+ try {
7751
+ resolved = def.thunk();
7752
+ } catch {
7753
+ resolved = void 0;
7754
+ }
7755
+ if (resolved === void 0) return p._ruleName !== void 0;
7756
+ return visit(resolved);
7757
+ }
7758
+ return childrenOf(def).some(visit);
7759
+ };
7760
+ return ruleMap.some(([, rule]) => visit(rule));
7761
+ }
7762
+ function hasSemanticReduction(roots, externalRefs) {
7427
7763
  const seen = /* @__PURE__ */ new Set();
7428
7764
  const visit = (parser2) => {
7429
7765
  if (seen.has(parser2)) return false;
@@ -7434,6 +7770,7 @@ function hasSemanticReduction(roots) {
7434
7770
  if (def.tag === "guard" || def.tag === "withCtx") return true;
7435
7771
  if (def.tag === "node" && def.build !== void 0) return true;
7436
7772
  if (def.tag === "lazy") {
7773
+ if (externalRefs?.has(parser2)) return false;
7437
7774
  try {
7438
7775
  return visit(def.thunk());
7439
7776
  } catch {
@@ -7525,6 +7862,7 @@ function childrenOf2(def) {
7525
7862
  case "label":
7526
7863
  case "field":
7527
7864
  case "not":
7865
+ case "peek":
7528
7866
  case "node":
7529
7867
  case "withCtx":
7530
7868
  case "expect":
@@ -7558,6 +7896,13 @@ function lazyTarget(c) {
7558
7896
  }
7559
7897
  var Unserializable = class extends Error {
7560
7898
  };
7899
+ function repeatOpts(min, max, trailing) {
7900
+ const parts = [];
7901
+ if (min !== 0) parts.push(`min: ${min}`);
7902
+ if (max !== void 0) parts.push(`max: ${max}`);
7903
+ if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
7904
+ return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
7905
+ }
7561
7906
  function serializeRuleMap(ruleMap, scanSkip) {
7562
7907
  try {
7563
7908
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -7661,6 +8006,7 @@ function evalRuleMapIR(ir) {
7661
8006
  "optional",
7662
8007
  "sepBy",
7663
8008
  "not",
8009
+ "peek",
7664
8010
  "node",
7665
8011
  "parser",
7666
8012
  "scanTo",
@@ -7693,6 +8039,7 @@ function evalRuleMapIR(ir) {
7693
8039
  optional,
7694
8040
  sepBy,
7695
8041
  not,
8042
+ peek,
7696
8043
  node,
7697
8044
  parser,
7698
8045
  scanTo,
@@ -7867,17 +8214,21 @@ ${outerDecls.map((d) => " " + d).join("\n")}
7867
8214
  return `_gch([${items.join(", ")}])`;
7868
8215
  }
7869
8216
  case "many":
7870
- return `many(${kid(def.parser)})`;
8217
+ return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
8218
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
8219
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
7871
8220
  case "oneOrMore":
7872
- return `oneOrMore(${kid(def.parser)})`;
8221
+ return def.min === 1 && def.max === void 0 ? `oneOrMore(${kid(def.parser)})` : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
7873
8222
  case "optional":
7874
8223
  return `optional(${kid(def.parser)})`;
7875
8224
  case "attempt":
7876
8225
  return `attempt(${kid(def.parser)})`;
7877
8226
  case "sepBy":
7878
- return `sepBy(${kid(def.parser)}, ${kid(def.separator)})`;
8227
+ return `sepBy(${kid(def.parser)}, ${kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`;
7879
8228
  case "not":
7880
8229
  return `not(${kid(def.parser)})`;
8230
+ case "peek":
8231
+ return `peek(${kid(def.parser)})`;
7881
8232
  case "trivia":
7882
8233
  return `trivia(${kid(def.parser)})`;
7883
8234
  case "token":
@@ -8070,6 +8421,9 @@ var COMPOSED_COVERAGE_RULES = /* @__PURE__ */ Symbol.for("parseman.composedCover
8070
8421
  function isIRPiece(p) {
8071
8422
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
8072
8423
  }
8424
+ function carriedRuleMaps(carried) {
8425
+ return carried.filter(isIRPiece).map((p) => evalRuleMapIR(p.ir));
8426
+ }
8073
8427
  function coverageRulesOf(carried) {
8074
8428
  const winners = {};
8075
8429
  for (const piece of carried) {
@@ -8154,6 +8508,7 @@ function compose(items) {
8154
8508
  const used = /* @__PURE__ */ new Set();
8155
8509
  const trivia2 = composingTriviaOf(items);
8156
8510
  const carried = items.flatMap((item) => itemCarried(item, used, trivia2));
8511
+ runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
8157
8512
  const pieces = carried.map((p) => materializePiece(p, trivia2));
8158
8513
  const map = fuseRules(pieces);
8159
8514
  Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false });
@@ -8986,7 +9341,7 @@ function coverageEntry(entry, collector, maps, trace) {
8986
9341
  };
8987
9342
  }
8988
9343
  case "sepBy":
8989
- return sepBy(build(def.parser), build(def.separator));
9344
+ 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
9345
  case "transform":
8991
9346
  return transform(build(def.parser), def.fn);
8992
9347
  case "skip":
@@ -9007,6 +9362,8 @@ function coverageEntry(entry, collector, maps, trace) {
9007
9362
  }, build(def.parser));
9008
9363
  case "not":
9009
9364
  return not(build(def.parser));
9365
+ case "peek":
9366
+ return peek(build(def.parser));
9010
9367
  case "node": {
9011
9368
  const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
9012
9369
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);