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.js CHANGED
@@ -1,4 +1,9 @@
1
1
  // src/combinators/first-set.ts
2
+ function resolveNamedRef(p, resolve) {
3
+ if (resolve === void 0) return void 0;
4
+ const name = p._ruleName;
5
+ return name === void 0 ? void 0 : resolve(name);
6
+ }
2
7
  function union(a, b) {
3
8
  if (a.kind === "any" || b.kind === "any") return { kind: "any" };
4
9
  if (a.kind === "empty") return b;
@@ -36,10 +41,10 @@ function any() {
36
41
  function empty() {
37
42
  return { kind: "empty" };
38
43
  }
39
- function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
44
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
40
45
  if (seen.has(p)) return true;
41
46
  seen.add(p);
42
- const me = (c) => matchesEmpty(c, seen);
47
+ const me = (c) => matchesEmpty(c, seen, resolve);
43
48
  const d = p._def;
44
49
  switch (d.tag) {
45
50
  case "literal":
@@ -56,8 +61,16 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
56
61
  case "many":
57
62
  case "optional":
58
63
  case "not":
64
+ case "peek":
59
65
  return true;
60
66
  // zero repetitions / absent / lookahead
67
+ // Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
68
+ // Any `min >= 1` requires that many ITEMS, so it is nullable only when the
69
+ // item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
70
+ // nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
71
+ // the gating diagnostic for a list that can never match empty.)
72
+ case "sepBy":
73
+ return d.min >= 1 ? me(d.parser) : true;
61
74
  case "oneOrMore":
62
75
  return me(d.parser);
63
76
  case "sequence":
@@ -81,7 +94,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
81
94
  try {
82
95
  return me(d.thunk());
83
96
  } catch {
84
- return true;
97
+ const t = resolveNamedRef(p, resolve);
98
+ return t ? me(t) : true;
85
99
  }
86
100
  default:
87
101
  return true;
@@ -90,18 +104,45 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
90
104
  function isZeroWidthAssertion(p) {
91
105
  return p._def.tag === "not";
92
106
  }
107
+ function isPositiveLookahead(p) {
108
+ return p._def.tag === "peek";
109
+ }
110
+ function narrowBy(acc, constraint) {
111
+ if (constraint.kind === "any") return acc;
112
+ if (acc === null) return constraint;
113
+ if (acc.kind === "any") return constraint;
114
+ if (acc.kind === "empty" || constraint.kind === "empty") return { kind: "empty" };
115
+ const ranges = [];
116
+ for (const a of acc.ranges) for (const b of constraint.ranges) {
117
+ const lo = Math.max(a.lo, b.lo);
118
+ const hi = Math.min(a.hi, b.hi);
119
+ if (lo <= hi) ranges.push({ lo, hi });
120
+ }
121
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
122
+ }
123
+ function applyAssertion(fs, assertion) {
124
+ if (assertion === null) return fs;
125
+ if (fs.kind === "empty") return assertion;
126
+ return narrowBy(fs, assertion) ?? fs;
127
+ }
93
128
  function sequenceFirstSet(parsers) {
94
129
  let fs = empty();
130
+ let assertion = null;
95
131
  for (const p of parsers) {
132
+ if (isPositiveLookahead(p)) {
133
+ assertion = narrowBy(assertion, p._meta.firstSet);
134
+ continue;
135
+ }
96
136
  if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet);
97
- if (!matchesEmpty(p)) return fs;
137
+ if (!matchesEmpty(p)) return applyAssertion(fs, assertion);
98
138
  }
99
- return fs;
139
+ return applyAssertion(fs, assertion);
100
140
  }
101
- function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
141
+ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve) {
102
142
  if (seen.has(p)) return any();
103
143
  seen.add(p);
104
- const fs = (c) => firstSetOf(c, seen);
144
+ const fs = (c) => firstSetOf(c, seen, resolve);
145
+ const empties = (c) => matchesEmpty(c, /* @__PURE__ */ new Set(), resolve);
105
146
  const d = p._def;
106
147
  switch (d.tag) {
107
148
  case "literal":
@@ -113,7 +154,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
113
154
  try {
114
155
  return fs(d.thunk());
115
156
  } catch {
116
- return any();
157
+ const t = resolveNamedRef(p, resolve);
158
+ return t ? fs(t) : any();
117
159
  }
118
160
  case "choice": {
119
161
  let out = empty();
@@ -122,11 +164,20 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
122
164
  }
123
165
  case "sequence": {
124
166
  let out = empty();
167
+ let assertion = null;
125
168
  for (const term of d.parsers) {
169
+ if (isPositiveLookahead(term)) {
170
+ assertion = narrowBy(assertion, fs(term));
171
+ continue;
172
+ }
126
173
  if (!isZeroWidthAssertion(term)) out = union(out, fs(term));
127
- if (!matchesEmpty(term)) return out;
174
+ if (!empties(term)) return applyAssertion(out, assertion);
128
175
  }
129
- return out;
176
+ return applyAssertion(out, assertion);
177
+ }
178
+ case "peek": {
179
+ const inner = d.parser;
180
+ return empties(inner) ? any() : fs(inner);
130
181
  }
131
182
  case "oneOrMore":
132
183
  case "many":
@@ -142,6 +193,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
142
193
  return fs(d.parser);
143
194
  case "sepBy":
144
195
  return fs(d.parser);
196
+ // both min 0 and min 1 start with the item
145
197
  case "skip":
146
198
  return fs(d.main);
147
199
  default:
@@ -283,28 +335,28 @@ function rollbackBufList(b, keyMulti, keySingle, len) {
283
335
  else if (len === 1) {
284
336
  b[keySingle] = arr[0];
285
337
  b[keyMulti] = void 0;
286
- } else arr.length = len;
338
+ } else if (arr.length !== len) arr.length = len;
287
339
  return;
288
340
  }
289
341
  if (len === 0) b[keySingle] = void 0;
290
342
  }
291
343
  function rollbackCstCapture(ctx, mark) {
292
- if (ctx._errors && mark.errors !== void 0) ctx._errors.length = mark.errors;
344
+ if (ctx._errors && mark.errors !== void 0 && ctx._errors.length !== mark.errors) ctx._errors.length = mark.errors;
293
345
  const b = ctx._cstBuf;
294
346
  if (b) {
295
347
  rollbackBufList(b, "raw", "rawSingle", mark.raw);
296
348
  rollbackBufList(b, "ch", "single", mark.leaves);
297
349
  if (b.tl) {
298
350
  if (mark.tlog === 0) b.tl = void 0;
299
- else b.tl.length = mark.tlog;
351
+ else if (b.tl.length !== mark.tlog) b.tl.length = mark.tlog;
300
352
  }
301
- if (ctx._fields) ctx._fields.length = mark.fields;
353
+ if (ctx._fields && ctx._fields.length !== mark.fields) ctx._fields.length = mark.fields;
302
354
  return;
303
355
  }
304
- if (ctx._cstRawChildren) ctx._cstRawChildren.length = mark.raw;
305
- if (ctx._cstTriviaLog) ctx._cstTriviaLog.length = mark.tlog;
306
- if (ctx._cstLeaves) ctx._cstLeaves.length = mark.leaves;
307
- if (ctx._fields) ctx._fields.length = mark.fields;
356
+ if (ctx._cstRawChildren && ctx._cstRawChildren.length !== mark.raw) ctx._cstRawChildren.length = mark.raw;
357
+ if (ctx._cstTriviaLog && ctx._cstTriviaLog.length !== mark.tlog) ctx._cstTriviaLog.length = mark.tlog;
358
+ if (ctx._cstLeaves && ctx._cstLeaves.length !== mark.leaves) ctx._cstLeaves.length = mark.leaves;
359
+ if (ctx._fields && ctx._fields.length !== mark.fields) ctx._fields.length = mark.fields;
308
360
  }
309
361
  function pushCstTriviaEntry(ctx, start, end, kindIndex) {
310
362
  const insertIdx = cstRawLen(ctx);
@@ -826,9 +878,46 @@ function firstSetFromRegex(source) {
826
878
  return { firstSet: firstSet(ast), canMatchNewline: firstCanBeNewline(ast) };
827
879
  }
828
880
 
881
+ // src/combinators/case-fold.ts
882
+ function canonical(ch2) {
883
+ const u = ch2.toUpperCase();
884
+ if (u.length !== 1) return ch2;
885
+ if (ch2.codePointAt(0) >= 128 && u.codePointAt(0) < 128) return ch2;
886
+ return u;
887
+ }
888
+ var foldIndex = null;
889
+ function buildFoldIndex() {
890
+ const byCanonical = /* @__PURE__ */ new Map();
891
+ for (let cp = 0; cp < 65536; cp++) {
892
+ if (cp >= 55296 && cp <= 57343) continue;
893
+ const key = canonical(String.fromCharCode(cp));
894
+ const bucket = byCanonical.get(key);
895
+ if (bucket) bucket.push(cp);
896
+ else byCanonical.set(key, [cp]);
897
+ }
898
+ const index = /* @__PURE__ */ new Map();
899
+ for (const members of byCanonical.values()) {
900
+ if (members.length < 2) continue;
901
+ for (const cp of members) index.set(cp, members);
902
+ }
903
+ return index;
904
+ }
905
+ function caseFoldVariants(cp) {
906
+ if (cp < 128) {
907
+ if (cp >= 65 && cp <= 90) return [cp, cp + 32];
908
+ if (cp >= 97 && cp <= 122) return [cp, cp - 32];
909
+ return [cp];
910
+ }
911
+ if (cp > 65535) return [cp];
912
+ foldIndex ??= buildFoldIndex();
913
+ return foldIndex.get(cp) ?? [cp];
914
+ }
915
+
829
916
  // src/combinators/keywords.ts
830
- function word(str, boundary = "_0-9A-Za-z") {
831
- return keywords([str], { boundary });
917
+ function word(str, boundaryOrOpts, opts) {
918
+ const boundary = typeof boundaryOrOpts === "string" ? boundaryOrOpts : "_0-9A-Za-z";
919
+ const rest = typeof boundaryOrOpts === "object" && boundaryOrOpts !== null ? boundaryOrOpts : opts;
920
+ return keywords([str], { ...rest, boundary });
832
921
  }
833
922
  function makeWord(boundary = "_0-9A-Za-z") {
834
923
  return (str) => keywords([str], { boundary });
@@ -840,7 +929,7 @@ function keywords(words, opts = {}) {
840
929
  const sorted = [...new Set(words)].sort((a, b) => b.length - a.length);
841
930
  const alt = sorted.map(escapeRe).join("|");
842
931
  const boundary = opts.boundary ? `(?![${opts.boundary}])` : "";
843
- const flags = opts.caseInsensitive ? "iuy" : "uy";
932
+ const flags = opts.caseInsensitive ? "iy" : "uy";
844
933
  const re = new RegExp(`(?:${alt})${boundary}`, flags);
845
934
  let firstSet2 = empty();
846
935
  for (const w of sorted) {
@@ -848,10 +937,7 @@ function keywords(words, opts = {}) {
848
937
  const cp = w.codePointAt(0);
849
938
  firstSet2 = union(firstSet2, fromChar(cp));
850
939
  if (opts.caseInsensitive) {
851
- const u = String.fromCodePoint(cp).toUpperCase().codePointAt(0);
852
- const l = String.fromCodePoint(cp).toLowerCase().codePointAt(0);
853
- if (u !== void 0) firstSet2 = union(firstSet2, fromChar(u));
854
- if (l !== void 0) firstSet2 = union(firstSet2, fromChar(l));
940
+ for (const v2 of caseFoldVariants(cp)) firstSet2 = union(firstSet2, fromChar(v2));
855
941
  }
856
942
  }
857
943
  const meta = { firstSet: firstSet2, canMatchNewline: false, isTrivia: false };
@@ -963,6 +1049,10 @@ var REC = { scan: recoverScan, at: matchesAt, or: orSentinel, sentinel: firstSet
963
1049
 
964
1050
  // src/combinators/expect.ts
965
1051
  function deriveExpected(c) {
1052
+ return derive(c, /* @__PURE__ */ new Set());
1053
+ }
1054
+ function derive(c, seen) {
1055
+ const deriveExpected2 = (p) => derive(p, seen);
966
1056
  const def = c._def;
967
1057
  switch (def.tag) {
968
1058
  case "literal":
@@ -974,20 +1064,32 @@ function deriveExpected(c) {
974
1064
  case "label":
975
1065
  return [def.label];
976
1066
  case "choice":
977
- return def.parsers.flatMap(deriveExpected);
978
- case "sequence":
979
- return def.parsers.length > 0 ? deriveExpected(def.parsers[0]) : [];
1067
+ return def.parsers.flatMap(deriveExpected2);
1068
+ // Union through the NULLABLE prefix, mirroring `sequenceFirstSet`. A leading
1069
+ // `optional(…)`/`many(…)` can match nothing, so the term after it is equally
1070
+ // able to fail first — deriving from term 0 alone named a token the parse
1071
+ // never actually required. A leading `not(…)` is zero-width and contributes
1072
+ // nothing (its own expectation is about what must NOT be here), but it is
1073
+ // nullable, so keep scanning past it. Stop at the first term that must match.
1074
+ case "sequence": {
1075
+ const out = [];
1076
+ for (const term of def.parsers) {
1077
+ if (!isZeroWidthAssertion(term)) out.push(...deriveExpected2(term));
1078
+ if (!matchesEmpty(term)) break;
1079
+ }
1080
+ return out;
1081
+ }
980
1082
  case "attempt":
981
- return deriveExpected(def.parser);
1083
+ return deriveExpected2(def.parser);
982
1084
  // Delegating wrappers whose first-token failure is their inner parser's — must
983
1085
  // pass through so a start-failure (and the first-set-miss fast-path in attempt/
984
1086
  // node, which reads this) names the real expected token, not the wrapper tag.
985
1087
  case "field":
986
1088
  case "withCtx":
987
1089
  case "recover":
988
- return deriveExpected(def.parser);
1090
+ return deriveExpected2(def.parser);
989
1091
  case "skip":
990
- return deriveExpected(def.main);
1092
+ return deriveExpected2(def.main);
991
1093
  case "expect":
992
1094
  return def.expected;
993
1095
  case "node":
@@ -1000,13 +1102,18 @@ function deriveExpected(c) {
1000
1102
  case "oneOrMore":
1001
1103
  case "transform":
1002
1104
  case "not":
1003
- return deriveExpected(def.parser);
1105
+ case "peek":
1106
+ return deriveExpected2(def.parser);
1004
1107
  case "lazy": {
1005
1108
  const name = c._ruleName;
1109
+ if (seen.has(c)) return name ? [name] : [];
1110
+ seen.add(c);
1006
1111
  try {
1007
- return deriveExpected(def.thunk());
1112
+ return deriveExpected2(def.thunk());
1008
1113
  } catch {
1009
1114
  return name ? [name] : [];
1115
+ } finally {
1116
+ seen.delete(c);
1010
1117
  }
1011
1118
  }
1012
1119
  default:
@@ -1137,14 +1244,14 @@ function choice(...args) {
1137
1244
  const result = parsers[i].parse(input, pos, ctx);
1138
1245
  if (!result.ok) {
1139
1246
  rollbackCstCapture(ctx, mark);
1140
- if (logLen !== void 0 && ctx._triviaLog) ctx._triviaLog.length = logLen;
1247
+ if (logLen !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== logLen) ctx._triviaLog.length = logLen;
1141
1248
  expected.push(...result.expected);
1142
1249
  continue;
1143
1250
  }
1144
1251
  const checks = autoNot[i];
1145
1252
  if (checks && autoNotFires(input, result.span.end, checks)) {
1146
1253
  rollbackCstCapture(ctx, mark);
1147
- if (logLen !== void 0 && ctx._triviaLog) ctx._triviaLog.length = logLen;
1254
+ if (logLen !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== logLen) ctx._triviaLog.length = logLen;
1148
1255
  continue;
1149
1256
  }
1150
1257
  return result;
@@ -1471,7 +1578,14 @@ function saveTriviaMark(ctx) {
1471
1578
  }
1472
1579
  function rollbackTrivia(ctx, mark) {
1473
1580
  rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors });
1474
- if (ctx._triviaLog) ctx._triviaLog.length = mark.log;
1581
+ if (ctx._triviaLog && ctx._triviaLog.length !== mark.log) ctx._triviaLog.length = mark.log;
1582
+ }
1583
+ function saveLookaheadMark(ctx) {
1584
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null };
1585
+ }
1586
+ function rollbackLookahead(ctx, mark) {
1587
+ rollbackTrivia(ctx, mark);
1588
+ if (ctx._probe) ctx._probe.best = mark.probeBest;
1475
1589
  }
1476
1590
  function scanWithLabels(input, cur, ctx) {
1477
1591
  const triviaP = ctx.trivia;
@@ -1832,7 +1946,7 @@ function attempt(parser2) {
1832
1946
  const result = parser2.parse(input, pos, ctx);
1833
1947
  if (result.ok) return result;
1834
1948
  rollbackCstCapture(ctx, mark);
1835
- if (log !== void 0 && ctx._triviaLog) ctx._triviaLog.length = log;
1949
+ if (log !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== log) ctx._triviaLog.length = log;
1836
1950
  return { ok: false, expected: result.expected, span: { start: pos, end: pos } };
1837
1951
  }
1838
1952
  };
@@ -1870,13 +1984,26 @@ function repItem(combinator, input, cur, ctx, guardable) {
1870
1984
  }
1871
1985
  return { value: result.value, end: result.span.end };
1872
1986
  }
1873
- function many(combinator) {
1987
+ function resolveBounds(what, opts) {
1988
+ const min = opts.min ?? 0;
1989
+ const max = opts.max ?? Infinity;
1990
+ const bad = (msg) => {
1991
+ throw new RangeError(`parseman: ${what} ${msg}`);
1992
+ };
1993
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`);
1994
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`);
1995
+ if (max < min) bad(`max (${max}) is less than min (${min}) \u2014 the combinator could never succeed`);
1996
+ return { min, max };
1997
+ }
1998
+ function many(combinator, opts = {}) {
1999
+ const { min, max } = resolveBounds("many()", opts);
2000
+ if (min >= 1) return atLeast(combinator, min, max);
1874
2001
  const meta = {
1875
2002
  firstSet: combinator._meta.firstSet,
1876
2003
  canMatchNewline: combinator._meta.canMatchNewline,
1877
2004
  isTrivia: false
1878
2005
  };
1879
- const def = { tag: "many", parser: combinator, min: 0 };
2006
+ const def = { tag: "many", parser: combinator, min: 0, ...max === Infinity ? {} : { max } };
1880
2007
  let expected;
1881
2008
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1882
2009
  return {
@@ -1886,7 +2013,9 @@ function many(combinator) {
1886
2013
  parse(input, pos, ctx) {
1887
2014
  const values = def.valueUnused ? void 0 : [];
1888
2015
  let cur = pos;
2016
+ let count = 0;
1889
2017
  while (cur < input.length) {
2018
+ if (count >= max) break;
1890
2019
  const item = repItem(combinator, input, cur, ctx, guardable);
1891
2020
  if (item === "stop") break;
1892
2021
  if ("fail" in item) {
@@ -1897,23 +2026,28 @@ function many(combinator) {
1897
2026
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1898
2027
  if (values !== void 0) values.push(error);
1899
2028
  captureError(ctx, error);
2029
+ count++;
1900
2030
  cur = end;
1901
2031
  continue;
1902
2032
  }
1903
2033
  if (values !== void 0) values.push(item.value);
2034
+ count++;
1904
2035
  cur = item.end;
1905
2036
  }
1906
2037
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1907
2038
  }
1908
2039
  };
1909
2040
  }
1910
- function oneOrMore(combinator) {
2041
+ function oneOrMore(combinator, opts = {}) {
2042
+ return many(combinator, { ...opts, min: opts.min ?? 1 });
2043
+ }
2044
+ function atLeast(combinator, min, max) {
1911
2045
  const meta = {
1912
2046
  firstSet: combinator._meta.firstSet,
1913
2047
  canMatchNewline: combinator._meta.canMatchNewline,
1914
2048
  isTrivia: false
1915
2049
  };
1916
- const def = { tag: "oneOrMore", parser: combinator, min: 1 };
2050
+ const def = { tag: "oneOrMore", parser: combinator, min, ...max === Infinity ? {} : { max } };
1917
2051
  let expected;
1918
2052
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1919
2053
  return {
@@ -1925,7 +2059,19 @@ function oneOrMore(combinator) {
1925
2059
  if (!first.ok) return first;
1926
2060
  const values = def.valueUnused ? void 0 : [first.value];
1927
2061
  let cur = first.span.end;
2062
+ let count = 1;
2063
+ while (count < min) {
2064
+ const item = repItem(combinator, input, cur, ctx, guardable);
2065
+ if (item === "stop" || "fail" in item) {
2066
+ expected ??= deriveExpected(combinator);
2067
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
2068
+ }
2069
+ if (values !== void 0) values.push(item.value);
2070
+ cur = item.end;
2071
+ count++;
2072
+ }
1928
2073
  while (cur < input.length) {
2074
+ if (count >= max) break;
1929
2075
  const item = repItem(combinator, input, cur, ctx, guardable);
1930
2076
  if (item === "stop") break;
1931
2077
  if ("fail" in item) {
@@ -1936,10 +2082,12 @@ function oneOrMore(combinator) {
1936
2082
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1937
2083
  if (values !== void 0) values.push(error);
1938
2084
  captureError(ctx, error);
2085
+ count++;
1939
2086
  cur = end;
1940
2087
  continue;
1941
2088
  }
1942
2089
  if (values !== void 0) values.push(item.value);
2090
+ count++;
1943
2091
  cur = item.end;
1944
2092
  }
1945
2093
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
@@ -1969,17 +2117,33 @@ function optional(combinator) {
1969
2117
  }
1970
2118
  };
1971
2119
  }
1972
- function sepBy(combinator, separator) {
2120
+ function oneOrMoreSep(combinator, separator, opts = {}) {
2121
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 });
2122
+ }
2123
+ function sepBy(combinator, separator, opts = {}) {
2124
+ const { min, max } = resolveBounds("sepBy()", opts);
2125
+ const trailing = opts.trailing ?? "forbid";
1973
2126
  const meta = {
1974
2127
  firstSet: combinator._meta.firstSet,
1975
2128
  canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
1976
2129
  isTrivia: false
1977
2130
  };
1978
2131
  let expected;
2132
+ const failAt2 = (at) => {
2133
+ expected ??= deriveExpected(combinator);
2134
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } };
2135
+ };
1979
2136
  return {
1980
2137
  _tag: "sepBy",
1981
2138
  _meta: meta,
1982
- _def: { tag: "sepBy", parser: combinator, separator },
2139
+ _def: {
2140
+ tag: "sepBy",
2141
+ parser: combinator,
2142
+ separator,
2143
+ min,
2144
+ ...max === Infinity ? {} : { max },
2145
+ ...trailing === "forbid" ? {} : { trailing }
2146
+ },
1983
2147
  parse(input, pos, ctx) {
1984
2148
  const first = combinator.parse(input, pos, ctx);
1985
2149
  const values = [];
@@ -1990,6 +2154,7 @@ function sepBy(combinator, separator) {
1990
2154
  } else {
1991
2155
  const term = ctx._tolerant ? ctx._sync : void 0;
1992
2156
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2157
+ if (min >= 1) return failAt2(pos);
1993
2158
  return { ok: true, value: [], span: { start: pos, end: pos } };
1994
2159
  }
1995
2160
  expected ??= deriveExpected(combinator);
@@ -1999,6 +2164,7 @@ function sepBy(combinator, separator) {
1999
2164
  cur = rec.end;
2000
2165
  }
2001
2166
  while (cur < input.length) {
2167
+ if (values.length >= max) break;
2002
2168
  const loopMark = saveTriviaMark(ctx);
2003
2169
  let sepPos = cur;
2004
2170
  if (ctx.trivia) {
@@ -2015,6 +2181,7 @@ function sepBy(combinator, separator) {
2015
2181
  rollbackTrivia(ctx, loopMark);
2016
2182
  break;
2017
2183
  }
2184
+ const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
2018
2185
  let nextPos = sep.span.end;
2019
2186
  if (ctx.trivia) {
2020
2187
  if (needsDeferredTriviaCommit(ctx)) {
@@ -2036,12 +2203,18 @@ function sepBy(combinator, separator) {
2036
2203
  cur = rec.end;
2037
2204
  continue;
2038
2205
  }
2206
+ if (sepMark !== void 0) {
2207
+ rollbackTrivia(ctx, sepMark);
2208
+ cur = sep.span.end;
2209
+ break;
2210
+ }
2039
2211
  rollbackTrivia(ctx, loopMark);
2040
2212
  break;
2041
2213
  }
2042
2214
  values.push(next.value);
2043
2215
  cur = next.span.end;
2044
2216
  }
2217
+ if (values.length < min) return failAt2(cur);
2045
2218
  return { ok: true, value: values, span: { start: pos, end: cur } };
2046
2219
  }
2047
2220
  };
@@ -2131,6 +2304,7 @@ function markUnusedValues(root) {
2131
2304
  visit(def.separator, false);
2132
2305
  return;
2133
2306
  case "not":
2307
+ case "peek":
2134
2308
  visit(def.parser, false);
2135
2309
  return;
2136
2310
  case "grammar":
@@ -2249,9 +2423,9 @@ function not(combinator) {
2249
2423
  _meta: meta,
2250
2424
  _def: { tag: "not", parser: combinator },
2251
2425
  parse(input, pos, ctx) {
2252
- const mark = saveCstMark(ctx);
2426
+ const mark = saveLookaheadMark(ctx);
2253
2427
  const result = combinator.parse(input, pos, ctx);
2254
- rollbackCstCapture(ctx, mark);
2428
+ rollbackLookahead(ctx, mark);
2255
2429
  if (result.ok) {
2256
2430
  return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } };
2257
2431
  }
@@ -2260,6 +2434,28 @@ function not(combinator) {
2260
2434
  };
2261
2435
  }
2262
2436
 
2437
+ // src/combinators/peek.ts
2438
+ function peek(combinator) {
2439
+ const meta = {
2440
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
2441
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
2442
+ canMatchNewline: false,
2443
+ isTrivia: false
2444
+ };
2445
+ return {
2446
+ _tag: "peek",
2447
+ _meta: meta,
2448
+ _def: { tag: "peek", parser: combinator },
2449
+ parse(input, pos, ctx) {
2450
+ const mark = saveLookaheadMark(ctx);
2451
+ const result = combinator.parse(input, pos, ctx);
2452
+ rollbackLookahead(ctx, mark);
2453
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } };
2454
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } };
2455
+ }
2456
+ };
2457
+ }
2458
+
2263
2459
  // src/compiler/build-arity.ts
2264
2460
  var PARAM_LIST_RE = (
2265
2461
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
@@ -2358,6 +2554,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2358
2554
  case "expect":
2359
2555
  case "withCtx":
2360
2556
  case "not":
2557
+ case "peek":
2361
2558
  return parserHasOwnFields(d.parser, seen);
2362
2559
  default:
2363
2560
  return false;
@@ -2397,6 +2594,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2397
2594
  case "expect":
2398
2595
  case "withCtx":
2399
2596
  case "not":
2597
+ case "peek":
2400
2598
  case "field":
2401
2599
  return parserHasTriviaSite(d.parser, seen);
2402
2600
  case "choice":
@@ -2446,6 +2644,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2446
2644
  case "expect":
2447
2645
  case "withCtx":
2448
2646
  case "not":
2647
+ case "peek":
2449
2648
  return parserEnablesTriviaCapture(d.parser, seen);
2450
2649
  // leaf() clears the inner CST/trivia collector and publishes only its own
2451
2650
  // terminal result. An inner parser() capture cannot reach this node.
@@ -2821,7 +3020,7 @@ function leaf(root, fn) {
2821
3020
  }
2822
3021
 
2823
3022
  // src/version.ts
2824
- var PARSEMAN_VERSION = "0.33.0";
3023
+ var PARSEMAN_VERSION = "0.35.0";
2825
3024
 
2826
3025
  // src/analysis/gating.ts
2827
3026
  var isAny = (fs) => fs.kind === "any";
@@ -2861,14 +3060,15 @@ function firstSetToString(fs) {
2861
3060
  var ruleNameOf = (p) => p._ruleName;
2862
3061
  var SUGGESTIONS = {
2863
3062
  "broad-recognizer": "if this arm leads with a keyword regex, use word('kw', boundary) / keywords([...]) \u2014 they expose an EXACT, gating first-set and lower to the same charCodeAt scan. A genuine scanTo fallback is fine; accept it in the gating snapshot if intentional.",
2864
- "leading-not": "let the arm lead with its actual consuming terminal (first-sets gate it automatically); keep not(...) only as a TRAILING boundary. Do not hand-roll not(not(...)) to fake gating.",
2865
- "nullable-prefix": "a leading optional/many lets a later, broad term start the arm. Split the empty case into its own arm, or gate on the prefix first char.",
3063
+ "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.",
3064
+ "nullable-prefix": "a leading optional/many lets a later, broad term start the arm. Split the empty case into its own arm, or gate on the prefix first char. A plain sepBy(item, sep) is ALSO nullable \u2014 pass { min: 1 } for a list that cannot be empty.",
2866
3065
  "cross-artifact-ref": "parseman >=0.32.0 resolves a g.Foo ref first-set at fuse time; if still ANY the target rule is itself ungated \u2014 analyze it and give it a concrete non-nullable lead.",
2867
3066
  "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2868
3067
  "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2869
3068
  };
2870
- function classifyBroadArm(arm) {
3069
+ function classifyBroadArm(arm, resolve) {
2871
3070
  const seen = /* @__PURE__ */ new Set();
3071
+ const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
2872
3072
  const walk2 = (p) => {
2873
3073
  if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2874
3074
  seen.add(p);
@@ -2880,6 +3080,10 @@ function classifyBroadArm(arm) {
2880
3080
  return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2881
3081
  case "not":
2882
3082
  return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
3083
+ // peek() CARRIES its body's first-set, so it is only broad when the body is
3084
+ // (or the body is nullable) — walk in for the real cause.
3085
+ case "peek":
3086
+ return walk2(d.parser);
2883
3087
  case "scanTo":
2884
3088
  return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2885
3089
  // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
@@ -2896,18 +3100,23 @@ function classifyBroadArm(arm) {
2896
3100
  return walk2(d.parser);
2897
3101
  case "lazy": {
2898
3102
  const name = p._ruleName;
3103
+ let target;
2899
3104
  try {
2900
- const inner = walk2(d.thunk());
2901
- if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
2902
- return inner;
3105
+ target = d.thunk();
2903
3106
  } catch {
2904
- return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3107
+ const bound = name !== void 0 ? resolve?.(name) : void 0;
3108
+ if (bound === void 0)
3109
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}`, unresolvedExternal: name !== void 0 };
3110
+ target = bound;
2905
3111
  }
3112
+ const inner = walk2(target);
3113
+ if (name !== void 0) return inner ? relabel(inner, "cross-artifact-ref", `via ref g.${name} \u2192 ${inner.detail}`) : null;
3114
+ return inner;
2906
3115
  }
2907
3116
  case "optional":
2908
3117
  case "many": {
2909
3118
  const inner = walk2(d.parser);
2910
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3119
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2911
3120
  }
2912
3121
  case "oneOrMore":
2913
3122
  case "transform":
@@ -2929,13 +3138,13 @@ function classifyBroadArm(arm) {
2929
3138
  sawNullablePrefix = true;
2930
3139
  continue;
2931
3140
  }
2932
- if (isAny(firstSetOf(t))) {
3141
+ if (isAny(firstSetOf(t, /* @__PURE__ */ new Set(), resolve))) {
2933
3142
  const inner = walk2(t);
2934
3143
  if (sawNullablePrefix)
2935
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3144
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2936
3145
  return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2937
3146
  }
2938
- if (matchesEmpty(t)) {
3147
+ if (matchesEmpty(t, /* @__PURE__ */ new Set(), resolve)) {
2939
3148
  sawNullablePrefix = true;
2940
3149
  continue;
2941
3150
  }
@@ -2946,7 +3155,7 @@ function classifyBroadArm(arm) {
2946
3155
  case "choice": {
2947
3156
  for (const a of d.parsers) {
2948
3157
  const r = walk2(a);
2949
- if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3158
+ if (r) return relabel(r, r.cause, `choice arm \u2192 ${r.detail}`);
2950
3159
  }
2951
3160
  return null;
2952
3161
  }
@@ -2983,7 +3192,7 @@ function detectAntiPatterns(rule, arms) {
2983
3192
  kind: "double-not",
2984
3193
  rule,
2985
3194
  armIndex: i,
2986
- message: "not(not(...)) hand-rolls automatic first-char gating and MISCOMPILES among shared-first-char sibling arms (its first-set is ANY, poisoning dispatch). Remove it; let the arm lead with its consuming terminal, or use word()/keywords() for an exact first-set."
3195
+ message: "not(not(...)) hand-rolls automatic first-char gating and MISCOMPILES among shared-first-char sibling arms (its first-set is ANY, poisoning dispatch). Prefer letting the arm lead with its consuming terminal; where a real positive lookahead IS needed, use peek(X) \u2014 zero-width like not(not(X)) but it CARRIES X's first-set, so the arm still dispatches."
2987
3196
  });
2988
3197
  } else {
2989
3198
  out.push({
@@ -3006,6 +3215,9 @@ function detectAntiPatterns(rule, arms) {
3006
3215
  return out;
3007
3216
  }
3008
3217
  function analyzeGating(entry, opts) {
3218
+ return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3219
+ }
3220
+ function analyzeGatingRules(ruleMap, opts) {
3009
3221
  const raw = [];
3010
3222
  const antiPatterns = [];
3011
3223
  const visited = /* @__PURE__ */ new Set();
@@ -3015,7 +3227,7 @@ function analyzeGating(entry, opts) {
3015
3227
  const d = p._def;
3016
3228
  const rule = ruleNameOf(p) ?? enclosingRule;
3017
3229
  if (d.tag === "choice") {
3018
- raw.push({ g: analyzeChoice(p, d, rule), rule });
3230
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
3019
3231
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3020
3232
  }
3021
3233
  const rec = d;
@@ -3032,7 +3244,7 @@ function analyzeGating(entry, opts) {
3032
3244
  }
3033
3245
  for (const k of kids) visit(k, rule);
3034
3246
  };
3035
- visit(entry, ruleNameOf(entry) ?? "<entry>");
3247
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3036
3248
  const perRule = /* @__PURE__ */ new Map();
3037
3249
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3038
3250
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3048,18 +3260,19 @@ function analyzeGating(entry, opts) {
3048
3260
  });
3049
3261
  const gated = choices.filter((c) => c.gates === "yes").length;
3050
3262
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3051
- const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3263
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted && !c.deferred);
3052
3264
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3265
+ const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3053
3266
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3054
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3267
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3055
3268
  }
3056
- function analyzeChoice(p, d, rule) {
3269
+ function analyzeChoice(p, d, rule, resolve) {
3057
3270
  const arms = d.parsers;
3058
3271
  const shallow = arms.map((a) => a._meta.firstSet);
3059
- const deep = arms.map((a) => firstSetOf(a));
3272
+ const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
3060
3273
  const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3061
- const { cause, detail } = classifyBroadArm(arms[index]);
3062
- return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3274
+ const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
3275
+ return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
3063
3276
  });
3064
3277
  const overlaps = [];
3065
3278
  for (let i = 0; i < deep.length; i++)
@@ -3075,10 +3288,12 @@ function analyzeChoice(p, d, rule) {
3075
3288
  if (d.disjoint) gates = "yes";
3076
3289
  else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3077
3290
  else gates = "no";
3291
+ const deferred = gates === "no" && overlaps.length === 0 && anyArms.length > 0 && anyArms.every((a) => a.unresolvedExternal);
3078
3292
  return {
3079
3293
  rule,
3080
3294
  strategy: d.strategy?.tag ?? "firstMatch",
3081
3295
  gates,
3296
+ deferred,
3082
3297
  combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3083
3298
  anyArms,
3084
3299
  overlaps
@@ -3245,6 +3460,7 @@ function children(def, winners) {
3245
3460
  case "field":
3246
3461
  case "grammar":
3247
3462
  case "not":
3463
+ case "peek":
3248
3464
  case "node":
3249
3465
  case "guard":
3250
3466
  case "withCtx":
@@ -4995,9 +5211,9 @@ function planDisjointDispatch(parsers) {
4995
5211
  return total >= SWITCH_MIN_CASES ? { kind: "switch", cases } : { kind: "if" };
4996
5212
  }
4997
5213
  function captureRestoreBody(mL, mR, mTl, mLg, mF = null) {
4998
- const fieldRestore = mF ? `; if (_ctx._fields) _ctx._fields.length = ${mF}` : "";
4999
- const base = `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${mL}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${mR}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${mTl}${fieldRestore}`;
5000
- return mLg ? `${base}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${mLg}` : base;
5214
+ const fieldRestore = mF ? `; if (_ctx._fields && _ctx._fields.length !== ${mF}) _ctx._fields.length = ${mF}` : "";
5215
+ 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}`;
5216
+ return mLg ? `${base}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${mLg}) _ctx._triviaLog.length = ${mLg}` : base;
5001
5217
  }
5002
5218
  function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5003
5219
  if (seen.has(p)) return false;
@@ -5010,6 +5226,9 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5010
5226
  case "keywords":
5011
5227
  case "guard":
5012
5228
  case "not":
5229
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
5230
+ // outcomes, so it can never leave a partial capture behind.
5231
+ case "peek":
5013
5232
  case "trivia":
5014
5233
  case "token":
5015
5234
  case "leaf":
@@ -5081,6 +5300,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
5081
5300
  case "leaf":
5082
5301
  return true;
5083
5302
  case "not":
5303
+ case "peek":
5084
5304
  case "guard":
5085
5305
  case "trivia":
5086
5306
  case "unknown":
@@ -5207,6 +5427,7 @@ function emitKeywordsFast(def, ctx, pos) {
5207
5427
  if (def.words.length === 0 || def.words.some((w) => w.length === 0)) return null;
5208
5428
  if (def.words.some((w) => Array.from(w).length !== w.length)) return null;
5209
5429
  if (def.caseInsensitive && def.boundary) return null;
5430
+ if (def.caseInsensitive && def.words.some((w) => w.split("").some((c) => c.charCodeAt(0) > 127))) return null;
5210
5431
  let boundary = null;
5211
5432
  if (def.boundary) {
5212
5433
  let body = def.boundary;
@@ -5254,7 +5475,7 @@ function emitKeywords(def, ctx, pos) {
5254
5475
  if (fast) return fast;
5255
5476
  const alt = def.words.map(escapeKeywordRe).join("|");
5256
5477
  const boundary = def.boundary ? `(?![${def.boundary}])` : "";
5257
- const flags = def.caseInsensitive ? "iuy" : "uy";
5478
+ const flags = def.caseInsensitive ? "iy" : "uy";
5258
5479
  const source = `(?:${alt})${boundary}`;
5259
5480
  const key = `${source}/${flags}`;
5260
5481
  let rName = ctx.regexMap.get(key);
@@ -5354,7 +5575,7 @@ function emitSeqValues(def, ctx, pos) {
5354
5575
  const endAfterV = v(ctx, "_sea");
5355
5576
  stmts.push(
5356
5577
  `${ind(ctx)}const ${endAfterV} = ${r2.endVar}`,
5357
- `${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}; }`
5578
+ `${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}; }`
5358
5579
  );
5359
5580
  valueVars.push(r2.valueVar);
5360
5581
  continue;
@@ -5400,6 +5621,7 @@ function failsAtStart(p) {
5400
5621
  case "keywords":
5401
5622
  case "guard":
5402
5623
  case "not":
5624
+ case "peek":
5403
5625
  return true;
5404
5626
  case "transform":
5405
5627
  case "label":
@@ -5813,14 +6035,17 @@ function emitMany(def, ctx, pos) {
5813
6035
  ];
5814
6036
  const mySyncV = ctx.recovery ? v(ctx, "_mysy") : "";
5815
6037
  if (ctx.recovery) stmts.push(`${ind(ctx)}const ${mySyncV} = _ctx._sync`);
5816
- if (def.min === 1) {
6038
+ for (let i = 0; i < def.min; i++) {
5817
6039
  const firstR = emit(def.parser, ctx, curV);
5818
6040
  stmts.push(...firstR.stmts);
5819
6041
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
5820
6042
  stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
5821
6043
  }
6044
+ const maxV = def.max === void 0 ? null : wantValue ? `${arrV}.length` : v(ctx, "_cnt");
6045
+ if (maxV !== null && !wantValue) stmts.push(`${ind(ctx)}let ${maxV} = ${def.min}`);
5822
6046
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
5823
6047
  ctx.indent++;
6048
+ if (maxV !== null) stmts.push(`${ind(ctx)}if (${maxV} >= ${def.max}) break`);
5824
6049
  let itemPos = curV;
5825
6050
  let rollback = "";
5826
6051
  if (ctx.activeTrivia) {
@@ -5837,7 +6062,7 @@ function emitMany(def, ctx, pos) {
5837
6062
  `${ind(ctx)}const ${npV} = ${capFn}(input, ${curV}, _ctx, 1)`
5838
6063
  );
5839
6064
  itemPos = npV;
5840
- rollback = `if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markV}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${markLog}; `;
6065
+ 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}; `;
5841
6066
  } else {
5842
6067
  const trivFn = ensureTriviaFn(ctx);
5843
6068
  const npV = v(ctx, "_np");
@@ -5861,7 +6086,7 @@ function emitMany(def, ctx, pos) {
5861
6086
  `${ind(ctx)}if (!${iterOk}) {`,
5862
6087
  `${ind(ctx)} ${rollback}if (_ctx._tolerant && ${mySyncV} !== undefined && !_ctx._rec.at(${mySyncV}, input, ${itemPos}, _ctx)) {`,
5863
6088
  `${ind(ctx)} const ${rrV} = _ctx._rec.scan(input, ${itemPos}, _ctx, ${mySyncV}, ${exp})`,
5864
- ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : [],
6089
+ ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : maxV !== null ? [`${ind(ctx)} ${maxV}++`] : [],
5865
6090
  `${ind(ctx)} _ctx._rec.capture(_ctx, ${rrV}.error)`,
5866
6091
  `${ind(ctx)} ${curV} = ${rrV}.end`,
5867
6092
  `${ind(ctx)} continue`,
@@ -5874,6 +6099,7 @@ function emitMany(def, ctx, pos) {
5874
6099
  stmts.push(`${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`);
5875
6100
  }
5876
6101
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
6102
+ else if (maxV !== null) stmts.push(`${ind(ctx)}${maxV}++`);
5877
6103
  stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
5878
6104
  ctx.indent--;
5879
6105
  stmts.push(`${ind(ctx)}}`);
@@ -5899,7 +6125,7 @@ function emitAttempt(p, def, ctx, pos) {
5899
6125
  const log = v(ctx, "_atg");
5900
6126
  const fields = v(ctx, "_atf");
5901
6127
  const errors = v(ctx, "_ate");
5902
- 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}`;
6128
+ 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}`;
5903
6129
  const traceId = ctx.coverage?.plan.attempts.get(p);
5904
6130
  const traceRollback = traceId === void 0 ? "" : ` _ctx._grammarTrace?.write({ id: ${JSON.stringify(traceId)}, phase: 'rollback', offset: ${pos} });`;
5905
6131
  const preGuard = [];
@@ -5938,8 +6164,8 @@ function emitSepBy(_p, def, ctx, pos) {
5938
6164
  ...rec ? [`${ind(ctx)}const ${mySyncV} = _ctx._sync`] : [],
5939
6165
  ...firstStmts
5940
6166
  ];
5941
- const failItem = (nextOkVar, itemPosVar, rb, originalFail) => {
5942
- if (!rec) return [originalFail];
6167
+ const failItem = (nextOkVar, itemPosVar, breakStmt) => {
6168
+ if (!rec) return [`${ind(ctx)}if (!${nextOkVar}) ${breakStmt === "break" ? "break" : `{ ${breakStmt} }`}`];
5943
6169
  const rr = v(ctx, "_rr");
5944
6170
  return [
5945
6171
  `${ind(ctx)}if (!${nextOkVar}) {`,
@@ -5947,10 +6173,24 @@ function emitSepBy(_p, def, ctx, pos) {
5947
6173
  `${ind(ctx)} const ${rr} = _ctx._rec.scan(input, ${itemPosVar}, _ctx, ${scanSync}, ${exp})`,
5948
6174
  `${ind(ctx)} ${arrV}.push(${rr}.error); _ctx._rec.capture(_ctx, ${rr}.error); ${curV} = ${rr}.end; continue`,
5949
6175
  `${ind(ctx)} }`,
5950
- `${ind(ctx)} ${rb}break`,
6176
+ `${ind(ctx)} ${breakStmt}`,
5951
6177
  `${ind(ctx)}}`
5952
6178
  ];
5953
6179
  };
6180
+ const itemFailBreak = (sepEndVar, sepRb, postSepRb) => def.trailing === void 0 ? `${sepRb}break` : `${postSepRb}${curV} = ${sepEndVar}; break`;
6181
+ const postSepMarks = () => {
6182
+ if (def.trailing === void 0 || !ctx.capturing) return { decl: [], rb: "" };
6183
+ const lv = v(ctx, "_tlv"), rw = v(ctx, "_trw"), tl = v(ctx, "_ttl"), lg = v(ctx, "_tlg");
6184
+ return {
6185
+ decl: [
6186
+ `${ind(ctx)}const ${lv} = _ctx._cstLeaves ? _ctx._cstLeaves.length : 0`,
6187
+ `${ind(ctx)}const ${rw} = _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0`,
6188
+ `${ind(ctx)}const ${tl} = _ctx._cstTriviaLog ? _ctx._cstTriviaLog.length : 0`,
6189
+ `${ind(ctx)}const ${lg} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`
6190
+ ],
6191
+ 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}; `
6192
+ };
6193
+ };
5954
6194
  if (rec) {
5955
6195
  const rr0 = v(ctx, "_rr0");
5956
6196
  const didV = v(ctx, "_did");
@@ -5976,6 +6216,7 @@ function emitSepBy(_p, def, ctx, pos) {
5976
6216
  );
5977
6217
  ctx.indent++;
5978
6218
  }
6219
+ if (def.max !== void 0) stmts.push(`${ind(ctx)}if (${arrV}.length >= ${def.max}) break`);
5979
6220
  let sepAtPos = curV;
5980
6221
  if (ctx.activeTrivia) {
5981
6222
  if (ctx.capturing) {
@@ -5993,15 +6234,17 @@ function emitSepBy(_p, def, ctx, pos) {
5993
6234
  `${ind(ctx)}const ${spV} = ${capFn}(input, ${curV}, _ctx, 1)`
5994
6235
  );
5995
6236
  sepAtPos = spV;
5996
- 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}; `;
6237
+ 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}; `;
5997
6238
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
5998
6239
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) { ${rollbackToSep}break }`);
6240
+ const post = postSepMarks();
6241
+ stmts.push(...post.decl);
5999
6242
  const npV = v(ctx, "_np");
6000
6243
  stmts.push(`${ind(ctx)}const ${npV} = ${capFn}(input, ${sepEnd}, _ctx, 1)`);
6001
6244
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6002
6245
  stmts.push(
6003
6246
  ...nextStmts,
6004
- ...failItem(nextOk, npV, rollbackToSep, `${ind(ctx)}if (!${nextOk}) { ${rollbackToSep}break }`),
6247
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, rollbackToSep, post.rb)),
6005
6248
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6006
6249
  `${ind(ctx)}${curV} = ${nextEnd}`
6007
6250
  );
@@ -6017,7 +6260,7 @@ function emitSepBy(_p, def, ctx, pos) {
6017
6260
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6018
6261
  stmts.push(
6019
6262
  ...nextStmts,
6020
- ...failItem(nextOk, npV, "", `${ind(ctx)}if (!${nextOk}) break`),
6263
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, "", "")),
6021
6264
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6022
6265
  `${ind(ctx)}${curV} = ${nextEnd}`
6023
6266
  );
@@ -6033,11 +6276,13 @@ function emitSepBy(_p, def, ctx, pos) {
6033
6276
  }
6034
6277
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6035
6278
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) break`);
6036
- const nextRb = markLv ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6279
+ 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}; ` : "";
6280
+ const post = postSepMarks();
6281
+ stmts.push(...post.decl);
6037
6282
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, sepEnd, true);
6038
6283
  stmts.push(
6039
6284
  ...nextStmts,
6040
- ...failItem(nextOk, sepEnd, nextRb, `${ind(ctx)}if (!${nextOk}) { ${nextRb}break }`),
6285
+ ...failItem(nextOk, sepEnd, itemFailBreak(sepEnd, nextRb, post.rb)),
6041
6286
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6042
6287
  `${ind(ctx)}${curV} = ${nextEnd}`
6043
6288
  );
@@ -6046,6 +6291,9 @@ function emitSepBy(_p, def, ctx, pos) {
6046
6291
  stmts.push(`${ind(ctx)}}`);
6047
6292
  ctx.indent--;
6048
6293
  stmts.push(`${ind(ctx)}}`);
6294
+ if (def.min >= 1) {
6295
+ stmts.push(...emitIfFail(ctx, `${arrV}.length < ${def.min}`, failArrBody(ctx, deriveExpectedArr([def.parser]), curV)));
6296
+ }
6049
6297
  return { stmts, valueVar: arrV, endVar: curV };
6050
6298
  }
6051
6299
  function emitScanTo(def, ctx, pos) {
@@ -6097,17 +6345,44 @@ function emitScanTo(def, ctx, pos) {
6097
6345
  return { stmts, valueVar: valV, endVar: curV };
6098
6346
  }
6099
6347
  function emitNot(def, ctx, pos) {
6348
+ const sinksLive = !!ctx.capturing || !!ctx.recovery;
6349
+ const leaves = sinksLive ? v(ctx, "_ntl") : null;
6350
+ const raw = sinksLive ? v(ctx, "_ntr") : null;
6351
+ const tl = sinksLive ? v(ctx, "_ntt") : null;
6352
+ const log = sinksLive ? v(ctx, "_ntg") : null;
6353
+ const fields = sinksLive ? v(ctx, "_ntf") : null;
6354
+ const errors = sinksLive ? v(ctx, "_nte") : null;
6100
6355
  const { stmts, okVar } = emitFallible(def.parser, ctx, pos, true);
6101
6356
  const label2 = JSON.stringify(`not(${def.parser._tag})`);
6102
6357
  return {
6103
6358
  stmts: [
6359
+ ...sinksLive ? [
6360
+ `${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`
6361
+ ] : [],
6104
6362
  ...stmts,
6363
+ ...sinksLive ? [
6364
+ `${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}`
6365
+ ] : [],
6105
6366
  ...emitIfFail(ctx, okVar, failBody(ctx, label2, pos))
6106
6367
  ],
6107
6368
  valueVar: "null",
6108
6369
  endVar: pos
6109
6370
  };
6110
6371
  }
6372
+ function emitPeek(def, ctx, pos) {
6373
+ const probeCtx = { ...ctx, capturing: false, noHoist: true };
6374
+ const { stmts, okVar } = emitFallible(def.parser, probeCtx, pos, true);
6375
+ ctx.vars = probeCtx.vars;
6376
+ const label2 = JSON.stringify(`peek(${def.parser._tag})`);
6377
+ return {
6378
+ stmts: [
6379
+ ...stmts,
6380
+ ...emitIfFail(ctx, `!${okVar}`, failBody(ctx, label2, pos))
6381
+ ],
6382
+ valueVar: "null",
6383
+ endVar: pos
6384
+ };
6385
+ }
6111
6386
  function escapeTokenLiteral(value) {
6112
6387
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6113
6388
  }
@@ -6395,9 +6670,9 @@ function emitRuntimeFallback(parser2, ctx, pos) {
6395
6670
  return { stmts, valueVar: vv, endVar: ev };
6396
6671
  }
6397
6672
  function emitLazy(p, def, ctx, pos) {
6398
- const canonical = ctx.ruleNames?.get(p);
6673
+ const canonical2 = ctx.ruleNames?.get(p);
6399
6674
  const usage = ctx.lazyUsage;
6400
- if (!canonical && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6675
+ if (!canonical2 && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6401
6676
  let resolved;
6402
6677
  try {
6403
6678
  resolved = def.thunk();
@@ -6407,7 +6682,7 @@ function emitLazy(p, def, ctx, pos) {
6407
6682
  return emit(resolved, ctx, pos);
6408
6683
  }
6409
6684
  if (!ctx.namedParsers.has(p)) {
6410
- const fnName2 = canonical ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6685
+ const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6411
6686
  ctx.namedParsers.set(p, fnName2);
6412
6687
  let resolved;
6413
6688
  try {
@@ -6690,6 +6965,8 @@ function emitDispatch(p, ctx, pos) {
6690
6965
  }
6691
6966
  case "not":
6692
6967
  return emitNot(def, ctx, pos);
6968
+ case "peek":
6969
+ return emitPeek(def, ctx, pos);
6693
6970
  case "node":
6694
6971
  return emitNode(def, ctx, pos);
6695
6972
  case "scanTo":
@@ -6778,6 +7055,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
6778
7055
  case "many":
6779
7056
  case "oneOrMore":
6780
7057
  case "not":
7058
+ case "peek":
6781
7059
  case "transform":
6782
7060
  return hasNodeDef(d.parser, seen);
6783
7061
  case "skip":
@@ -6829,17 +7107,18 @@ function childrenOf(def) {
6829
7107
  case "field":
6830
7108
  case "grammar":
6831
7109
  case "not":
7110
+ case "peek":
6832
7111
  case "node":
6833
7112
  case "withCtx":
6834
7113
  case "expect":
6835
7114
  return [def.parser];
6836
- // emitMany's oneOrMore branch and emitSepBy both codegen `def.parser` TWICE
6837
- // (a mandatory first match, then again inside the repeat loop) — two real
6838
- // emit() call sites, so the usage analysis must see two edges here or it
7115
+ // emitMany's min>=1 branch and emitSepBy both codegen `def.parser` more than
7116
+ // once (`min` mandatory matches, then again inside the repeat loop) — each is a
7117
+ // real emit() call site, so the usage analysis must see one edge per site or it
6839
7118
  // undercounts a single-use `parser` ref as inline-safe when it's actually
6840
- // referenced from two positions within this one compiled function.
7119
+ // referenced from several positions within this one compiled function.
6841
7120
  case "oneOrMore":
6842
- return [def.parser, def.parser];
7121
+ return Array.from({ length: def.min + 1 }, () => def.parser);
6843
7122
  case "sepBy":
6844
7123
  return [def.parser, def.parser, def.separator];
6845
7124
  case "skip":
@@ -6985,12 +7264,41 @@ function resolveGatingLevel(opt) {
6985
7264
  return "warn";
6986
7265
  }
6987
7266
  function runGatingDiagnostic(combinator, opt) {
7267
+ return reportGating(opt, (o) => analyzeGating(combinator, o));
7268
+ }
7269
+ function runGatingDiagnosticRules(ruleMap, opt) {
7270
+ return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7271
+ }
7272
+ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7273
+ if (resolveGatingLevel(opt) === "off") return void 0;
7274
+ const ruleMaps = getRuleMaps();
7275
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
7276
+ const winners = /* @__PURE__ */ new Map();
7277
+ for (const map of ruleMaps) for (const [name, rule] of map) {
7278
+ if (rule._def.tag === "lazy") {
7279
+ try {
7280
+ rule._def.thunk();
7281
+ } catch {
7282
+ continue;
7283
+ }
7284
+ }
7285
+ winners.set(name, rule);
7286
+ }
7287
+ const entries = [...winners];
7288
+ return reportGating(opt, (o) => {
7289
+ const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7290
+ const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7291
+ return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
7292
+ });
7293
+ }
7294
+ function reportGating(opt, analyze) {
6988
7295
  const level = resolveGatingLevel(opt);
6989
7296
  if (level === "off") return void 0;
6990
- const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7297
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
7298
+ const analyzeOpts = obj?.accept !== void 0 || obj?.entryName !== void 0 ? { ...obj.accept !== void 0 ? { accept: obj.accept } : {}, ...obj.entryName !== void 0 ? { entryName: obj.entryName } : {} } : void 0;
6991
7299
  let report;
6992
7300
  try {
6993
- report = analyzeGating(combinator, accept ? { accept } : void 0);
7301
+ report = analyze(analyzeOpts);
6994
7302
  } catch {
6995
7303
  return void 0;
6996
7304
  }
@@ -7127,6 +7435,7 @@ function publicRuleWrapperSource(rule, fnSource) {
7127
7435
  }
7128
7436
  function compileLinkable(ruleMapArg, ns, opts) {
7129
7437
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7438
+ if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
7130
7439
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7131
7440
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7132
7441
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
@@ -7183,6 +7492,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7183
7492
  fnNameToKey.set(fn, key);
7184
7493
  }
7185
7494
  ctx.ruleNames = ruleNames;
7495
+ const externalRefs = /* @__PURE__ */ new Set();
7186
7496
  {
7187
7497
  const scanned = /* @__PURE__ */ new Set();
7188
7498
  const scanExternal = (p) => {
@@ -7198,11 +7508,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
7198
7508
  }
7199
7509
  if (resolved === void 0) {
7200
7510
  const name = p._ruleName;
7201
- if (name && !ctx.namedParsers.has(p)) {
7202
- assertRuleName(name);
7203
- const fn = `_r_${name}`;
7204
- ruleNames.set(p, fn);
7205
- ctx.namedParsers.set(p, fn);
7511
+ if (name) {
7512
+ externalRefs.add(p);
7513
+ if (!ctx.namedParsers.has(p)) {
7514
+ assertRuleName(name);
7515
+ const fn = `_r_${name}`;
7516
+ ruleNames.set(p, fn);
7517
+ ctx.namedParsers.set(p, fn);
7518
+ }
7206
7519
  }
7207
7520
  return;
7208
7521
  }
@@ -7315,12 +7628,32 @@ function compileLinkable(ruleMapArg, ns, opts) {
7315
7628
  needsEmptyTl: !!ctx.needsEmptyTl,
7316
7629
  needsHostReads: !!ctx.needsHostReads,
7317
7630
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7318
- isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule)),
7631
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7319
7632
  mfFns: mfSrcs ? [] : ctx.mapFns,
7320
7633
  buildFns: buildSrcs ? [] : ctx.buildFns
7321
7634
  };
7322
7635
  }
7323
- function hasSemanticReduction(roots) {
7636
+ function hasExternalRuleRef(ruleMap) {
7637
+ const seen = /* @__PURE__ */ new Set();
7638
+ const visit = (p) => {
7639
+ if (seen.has(p)) return false;
7640
+ seen.add(p);
7641
+ const def = p._def;
7642
+ if (def.tag === "lazy") {
7643
+ let resolved;
7644
+ try {
7645
+ resolved = def.thunk();
7646
+ } catch {
7647
+ resolved = void 0;
7648
+ }
7649
+ if (resolved === void 0) return p._ruleName !== void 0;
7650
+ return visit(resolved);
7651
+ }
7652
+ return childrenOf(def).some(visit);
7653
+ };
7654
+ return ruleMap.some(([, rule]) => visit(rule));
7655
+ }
7656
+ function hasSemanticReduction(roots, externalRefs) {
7324
7657
  const seen = /* @__PURE__ */ new Set();
7325
7658
  const visit = (parser2) => {
7326
7659
  if (seen.has(parser2)) return false;
@@ -7331,6 +7664,7 @@ function hasSemanticReduction(roots) {
7331
7664
  if (def.tag === "guard" || def.tag === "withCtx") return true;
7332
7665
  if (def.tag === "node" && def.build !== void 0) return true;
7333
7666
  if (def.tag === "lazy") {
7667
+ if (externalRefs?.has(parser2)) return false;
7334
7668
  try {
7335
7669
  return visit(def.thunk());
7336
7670
  } catch {
@@ -7422,6 +7756,7 @@ function childrenOf2(def) {
7422
7756
  case "label":
7423
7757
  case "field":
7424
7758
  case "not":
7759
+ case "peek":
7425
7760
  case "node":
7426
7761
  case "withCtx":
7427
7762
  case "expect":
@@ -7455,6 +7790,13 @@ function lazyTarget(c) {
7455
7790
  }
7456
7791
  var Unserializable = class extends Error {
7457
7792
  };
7793
+ function repeatOpts(min, max, trailing) {
7794
+ const parts = [];
7795
+ if (min !== 0) parts.push(`min: ${min}`);
7796
+ if (max !== void 0) parts.push(`max: ${max}`);
7797
+ if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
7798
+ return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
7799
+ }
7458
7800
  function serializeRuleMap(ruleMap, scanSkip) {
7459
7801
  try {
7460
7802
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -7558,6 +7900,7 @@ function evalRuleMapIR(ir) {
7558
7900
  "optional",
7559
7901
  "sepBy",
7560
7902
  "not",
7903
+ "peek",
7561
7904
  "node",
7562
7905
  "parser",
7563
7906
  "scanTo",
@@ -7590,6 +7933,7 @@ function evalRuleMapIR(ir) {
7590
7933
  optional,
7591
7934
  sepBy,
7592
7935
  not,
7936
+ peek,
7593
7937
  node,
7594
7938
  parser,
7595
7939
  scanTo,
@@ -7764,17 +8108,21 @@ ${outerDecls.map((d) => " " + d).join("\n")}
7764
8108
  return `_gch([${items.join(", ")}])`;
7765
8109
  }
7766
8110
  case "many":
7767
- return `many(${kid(def.parser)})`;
8111
+ return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
8112
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
8113
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
7768
8114
  case "oneOrMore":
7769
- return `oneOrMore(${kid(def.parser)})`;
8115
+ return def.min === 1 && def.max === void 0 ? `oneOrMore(${kid(def.parser)})` : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
7770
8116
  case "optional":
7771
8117
  return `optional(${kid(def.parser)})`;
7772
8118
  case "attempt":
7773
8119
  return `attempt(${kid(def.parser)})`;
7774
8120
  case "sepBy":
7775
- return `sepBy(${kid(def.parser)}, ${kid(def.separator)})`;
8121
+ return `sepBy(${kid(def.parser)}, ${kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`;
7776
8122
  case "not":
7777
8123
  return `not(${kid(def.parser)})`;
8124
+ case "peek":
8125
+ return `peek(${kid(def.parser)})`;
7778
8126
  case "trivia":
7779
8127
  return `trivia(${kid(def.parser)})`;
7780
8128
  case "token":
@@ -7967,6 +8315,9 @@ var COMPOSED_COVERAGE_RULES = /* @__PURE__ */ Symbol.for("parseman.composedCover
7967
8315
  function isIRPiece(p) {
7968
8316
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
7969
8317
  }
8318
+ function carriedRuleMaps(carried) {
8319
+ return carried.filter(isIRPiece).map((p) => evalRuleMapIR(p.ir));
8320
+ }
7970
8321
  function coverageRulesOf(carried) {
7971
8322
  const winners = {};
7972
8323
  for (const piece of carried) {
@@ -8051,6 +8402,7 @@ function compose(items) {
8051
8402
  const used = /* @__PURE__ */ new Set();
8052
8403
  const trivia2 = composingTriviaOf(items);
8053
8404
  const carried = items.flatMap((item) => itemCarried(item, used, trivia2));
8405
+ runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
8054
8406
  const pieces = carried.map((p) => materializePiece(p, trivia2));
8055
8407
  const map = fuseRules(pieces);
8056
8408
  Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false });
@@ -8883,7 +9235,7 @@ function coverageEntry(entry, collector, maps, trace) {
8883
9235
  };
8884
9236
  }
8885
9237
  case "sepBy":
8886
- return sepBy(build(def.parser), build(def.separator));
9238
+ 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 } });
8887
9239
  case "transform":
8888
9240
  return transform(build(def.parser), def.fn);
8889
9241
  case "skip":
@@ -8904,6 +9256,8 @@ function coverageEntry(entry, collector, maps, trace) {
8904
9256
  }, build(def.parser));
8905
9257
  case "not":
8906
9258
  return not(build(def.parser));
9259
+ case "peek":
9260
+ return peek(build(def.parser));
8907
9261
  case "node": {
8908
9262
  const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
8909
9263
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
@@ -9278,6 +9632,7 @@ export {
9278
9632
  absolutize,
9279
9633
  absolutizeCST,
9280
9634
  analyzeGating,
9635
+ analyzeGatingRules,
9281
9636
  annotateSpan,
9282
9637
  applyEdit,
9283
9638
  attempt,
@@ -9325,10 +9680,12 @@ export {
9325
9680
  not,
9326
9681
  offsetToLineCol,
9327
9682
  oneOrMore,
9683
+ oneOrMoreSep,
9328
9684
  optional,
9329
9685
  parse,
9330
9686
  parseDoc,
9331
9687
  parser,
9688
+ peek,
9332
9689
  ref,
9333
9690
  regex,
9334
9691
  relativize,