parseman 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/analysis/gating.d.ts +66 -1
  2. package/dist/analysis/gating.d.ts.map +1 -1
  3. package/dist/combinators/case-fold.d.ts +39 -0
  4. package/dist/combinators/case-fold.d.ts.map +1 -0
  5. package/dist/combinators/expect.d.ts.map +1 -1
  6. package/dist/combinators/first-set.d.ts +38 -11
  7. package/dist/combinators/first-set.d.ts.map +1 -1
  8. package/dist/combinators/keywords.d.ts +12 -3
  9. package/dist/combinators/keywords.d.ts.map +1 -1
  10. package/dist/combinators/not.d.ts.map +1 -1
  11. package/dist/combinators/peek.d.ts +27 -0
  12. package/dist/combinators/peek.d.ts.map +1 -0
  13. package/dist/combinators/repeat.d.ts +68 -3
  14. package/dist/combinators/repeat.d.ts.map +1 -1
  15. package/dist/combinators/trivia-skip.d.ts +23 -1
  16. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  17. package/dist/compiler/codegen.d.ts +40 -0
  18. package/dist/compiler/codegen.d.ts.map +1 -1
  19. package/dist/compiler/fields.d.ts.map +1 -1
  20. package/dist/compiler/ir-serialize.d.ts +1 -1
  21. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  22. package/dist/compiler/linker.d.ts +5 -0
  23. package/dist/compiler/linker.d.ts.map +1 -1
  24. package/dist/compiler/value-usage.d.ts.map +1 -1
  25. package/dist/coverage.d.ts.map +1 -1
  26. package/dist/index.cjs +459 -86
  27. package/dist/index.cjs.map +4 -4
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +459 -86
  31. package/dist/index.js.map +4 -4
  32. package/dist/language-service/index.cjs.map +1 -1
  33. package/dist/language-service/index.js.map +1 -1
  34. package/dist/plugin/evaluator.d.ts.map +1 -1
  35. package/dist/plugin/index.cjs +538 -123
  36. package/dist/plugin/index.cjs.map +4 -4
  37. package/dist/plugin/index.d.ts.map +1 -1
  38. package/dist/plugin/index.js +538 -123
  39. package/dist/plugin/index.js.map +4 -4
  40. package/dist/spec/ebnf.d.ts.map +1 -1
  41. package/dist/spec/index.cjs +79 -17
  42. package/dist/spec/index.cjs.map +2 -2
  43. package/dist/spec/index.js +79 -17
  44. package/dist/spec/index.js.map +2 -2
  45. package/dist/spec/model.d.ts +23 -3
  46. package/dist/spec/model.d.ts.map +1 -1
  47. package/dist/spec/railroad.d.ts.map +1 -1
  48. package/dist/types.d.ts +9 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/package.json +5 -1
@@ -42,6 +42,11 @@ var import_oxc_resolver = require("oxc-resolver");
42
42
  var import_magic_string = __toESM(require("magic-string"), 1);
43
43
 
44
44
  // src/combinators/first-set.ts
45
+ function resolveNamedRef(p, resolve2) {
46
+ if (resolve2 === void 0) return void 0;
47
+ const name = p._ruleName;
48
+ return name === void 0 ? void 0 : resolve2(name);
49
+ }
45
50
  function union(a, b) {
46
51
  if (a.kind === "any" || b.kind === "any") return { kind: "any" };
47
52
  if (a.kind === "empty") return b;
@@ -79,10 +84,10 @@ function any() {
79
84
  function empty() {
80
85
  return { kind: "empty" };
81
86
  }
82
- function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
87
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve2) {
83
88
  if (seen.has(p)) return true;
84
89
  seen.add(p);
85
- const me = (c) => matchesEmpty(c, seen);
90
+ const me = (c) => matchesEmpty(c, seen, resolve2);
86
91
  const d = p._def;
87
92
  switch (d.tag) {
88
93
  case "literal":
@@ -99,8 +104,16 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
99
104
  case "many":
100
105
  case "optional":
101
106
  case "not":
107
+ case "peek":
102
108
  return true;
103
109
  // zero repetitions / absent / lookahead
110
+ // Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
111
+ // Any `min >= 1` requires that many ITEMS, so it is nullable only when the
112
+ // item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
113
+ // nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
114
+ // the gating diagnostic for a list that can never match empty.)
115
+ case "sepBy":
116
+ return d.min >= 1 ? me(d.parser) : true;
104
117
  case "oneOrMore":
105
118
  return me(d.parser);
106
119
  case "sequence":
@@ -124,7 +137,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
124
137
  try {
125
138
  return me(d.thunk());
126
139
  } catch {
127
- return true;
140
+ const t = resolveNamedRef(p, resolve2);
141
+ return t ? me(t) : true;
128
142
  }
129
143
  default:
130
144
  return true;
@@ -133,18 +147,45 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
133
147
  function isZeroWidthAssertion(p) {
134
148
  return p._def.tag === "not";
135
149
  }
150
+ function isPositiveLookahead(p) {
151
+ return p._def.tag === "peek";
152
+ }
153
+ function narrowBy(acc, constraint) {
154
+ if (constraint.kind === "any") return acc;
155
+ if (acc === null) return constraint;
156
+ if (acc.kind === "any") return constraint;
157
+ if (acc.kind === "empty" || constraint.kind === "empty") return { kind: "empty" };
158
+ const ranges = [];
159
+ for (const a of acc.ranges) for (const b of constraint.ranges) {
160
+ const lo = Math.max(a.lo, b.lo);
161
+ const hi = Math.min(a.hi, b.hi);
162
+ if (lo <= hi) ranges.push({ lo, hi });
163
+ }
164
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
165
+ }
166
+ function applyAssertion(fs2, assertion) {
167
+ if (assertion === null) return fs2;
168
+ if (fs2.kind === "empty") return assertion;
169
+ return narrowBy(fs2, assertion) ?? fs2;
170
+ }
136
171
  function sequenceFirstSet(parsers) {
137
172
  let fs2 = empty();
173
+ let assertion = null;
138
174
  for (const p of parsers) {
175
+ if (isPositiveLookahead(p)) {
176
+ assertion = narrowBy(assertion, p._meta.firstSet);
177
+ continue;
178
+ }
139
179
  if (!isZeroWidthAssertion(p)) fs2 = union(fs2, p._meta.firstSet);
140
- if (!matchesEmpty(p)) return fs2;
180
+ if (!matchesEmpty(p)) return applyAssertion(fs2, assertion);
141
181
  }
142
- return fs2;
182
+ return applyAssertion(fs2, assertion);
143
183
  }
144
- function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
184
+ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve2) {
145
185
  if (seen.has(p)) return any();
146
186
  seen.add(p);
147
- const fs2 = (c) => firstSetOf(c, seen);
187
+ const fs2 = (c) => firstSetOf(c, seen, resolve2);
188
+ const empties = (c) => matchesEmpty(c, /* @__PURE__ */ new Set(), resolve2);
148
189
  const d = p._def;
149
190
  switch (d.tag) {
150
191
  case "literal":
@@ -156,7 +197,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
156
197
  try {
157
198
  return fs2(d.thunk());
158
199
  } catch {
159
- return any();
200
+ const t = resolveNamedRef(p, resolve2);
201
+ return t ? fs2(t) : any();
160
202
  }
161
203
  case "choice": {
162
204
  let out = empty();
@@ -165,11 +207,20 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
165
207
  }
166
208
  case "sequence": {
167
209
  let out = empty();
210
+ let assertion = null;
168
211
  for (const term of d.parsers) {
212
+ if (isPositiveLookahead(term)) {
213
+ assertion = narrowBy(assertion, fs2(term));
214
+ continue;
215
+ }
169
216
  if (!isZeroWidthAssertion(term)) out = union(out, fs2(term));
170
- if (!matchesEmpty(term)) return out;
217
+ if (!empties(term)) return applyAssertion(out, assertion);
171
218
  }
172
- return out;
219
+ return applyAssertion(out, assertion);
220
+ }
221
+ case "peek": {
222
+ const inner = d.parser;
223
+ return empties(inner) ? any() : fs2(inner);
173
224
  }
174
225
  case "oneOrMore":
175
226
  case "many":
@@ -185,6 +236,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
185
236
  return fs2(d.parser);
186
237
  case "sepBy":
187
238
  return fs2(d.parser);
239
+ // both min 0 and min 1 start with the item
188
240
  case "skip":
189
241
  return fs2(d.main);
190
242
  default:
@@ -906,9 +958,46 @@ function firstSetFromRegex(source) {
906
958
  return { firstSet: firstSet(ast), canMatchNewline: firstCanBeNewline(ast) };
907
959
  }
908
960
 
961
+ // src/combinators/case-fold.ts
962
+ function canonical(ch2) {
963
+ const u = ch2.toUpperCase();
964
+ if (u.length !== 1) return ch2;
965
+ if (ch2.codePointAt(0) >= 128 && u.codePointAt(0) < 128) return ch2;
966
+ return u;
967
+ }
968
+ var foldIndex = null;
969
+ function buildFoldIndex() {
970
+ const byCanonical = /* @__PURE__ */ new Map();
971
+ for (let cp = 0; cp < 65536; cp++) {
972
+ if (cp >= 55296 && cp <= 57343) continue;
973
+ const key = canonical(String.fromCharCode(cp));
974
+ const bucket = byCanonical.get(key);
975
+ if (bucket) bucket.push(cp);
976
+ else byCanonical.set(key, [cp]);
977
+ }
978
+ const index = /* @__PURE__ */ new Map();
979
+ for (const members of byCanonical.values()) {
980
+ if (members.length < 2) continue;
981
+ for (const cp of members) index.set(cp, members);
982
+ }
983
+ return index;
984
+ }
985
+ function caseFoldVariants(cp) {
986
+ if (cp < 128) {
987
+ if (cp >= 65 && cp <= 90) return [cp, cp + 32];
988
+ if (cp >= 97 && cp <= 122) return [cp, cp - 32];
989
+ return [cp];
990
+ }
991
+ if (cp > 65535) return [cp];
992
+ foldIndex ??= buildFoldIndex();
993
+ return foldIndex.get(cp) ?? [cp];
994
+ }
995
+
909
996
  // src/combinators/keywords.ts
910
- function word(str, boundary = "_0-9A-Za-z") {
911
- return keywords([str], { boundary });
997
+ function word(str, boundaryOrOpts, opts) {
998
+ const boundary = typeof boundaryOrOpts === "string" ? boundaryOrOpts : "_0-9A-Za-z";
999
+ const rest = typeof boundaryOrOpts === "object" && boundaryOrOpts !== null ? boundaryOrOpts : opts;
1000
+ return keywords([str], { ...rest, boundary });
912
1001
  }
913
1002
  function escapeRe(s) {
914
1003
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -917,7 +1006,7 @@ function keywords(words, opts = {}) {
917
1006
  const sorted = [...new Set(words)].sort((a, b) => b.length - a.length);
918
1007
  const alt = sorted.map(escapeRe).join("|");
919
1008
  const boundary = opts.boundary ? `(?![${opts.boundary}])` : "";
920
- const flags = opts.caseInsensitive ? "iuy" : "uy";
1009
+ const flags = opts.caseInsensitive ? "iy" : "uy";
921
1010
  const re = new RegExp(`(?:${alt})${boundary}`, flags);
922
1011
  let firstSet2 = empty();
923
1012
  for (const w of sorted) {
@@ -925,10 +1014,7 @@ function keywords(words, opts = {}) {
925
1014
  const cp = w.codePointAt(0);
926
1015
  firstSet2 = union(firstSet2, fromChar(cp));
927
1016
  if (opts.caseInsensitive) {
928
- const u = String.fromCodePoint(cp).toUpperCase().codePointAt(0);
929
- const l = String.fromCodePoint(cp).toLowerCase().codePointAt(0);
930
- if (u !== void 0) firstSet2 = union(firstSet2, fromChar(u));
931
- if (l !== void 0) firstSet2 = union(firstSet2, fromChar(l));
1017
+ for (const v2 of caseFoldVariants(cp)) firstSet2 = union(firstSet2, fromChar(v2));
932
1018
  }
933
1019
  }
934
1020
  const meta = { firstSet: firstSet2, canMatchNewline: false, isTrivia: false };
@@ -1076,6 +1162,7 @@ function deriveExpected(c) {
1076
1162
  case "oneOrMore":
1077
1163
  case "transform":
1078
1164
  case "not":
1165
+ case "peek":
1079
1166
  return deriveExpected(def.parser);
1080
1167
  case "lazy": {
1081
1168
  const name = c._ruleName;
@@ -1546,6 +1633,13 @@ function rollbackTrivia(ctx, mark) {
1546
1633
  rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors });
1547
1634
  if (ctx._triviaLog) ctx._triviaLog.length = mark.log;
1548
1635
  }
1636
+ function saveLookaheadMark(ctx) {
1637
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null };
1638
+ }
1639
+ function rollbackLookahead(ctx, mark) {
1640
+ rollbackTrivia(ctx, mark);
1641
+ if (ctx._probe) ctx._probe.best = mark.probeBest;
1642
+ }
1549
1643
  function scanWithLabels(input, cur, ctx) {
1550
1644
  const triviaP = ctx.trivia;
1551
1645
  const spec = analyzeLabeledTrivia(triviaP);
@@ -1943,13 +2037,26 @@ function repItem(combinator, input, cur, ctx, guardable) {
1943
2037
  }
1944
2038
  return { value: result.value, end: result.span.end };
1945
2039
  }
1946
- function many(combinator) {
2040
+ function resolveBounds(what, opts) {
2041
+ const min = opts.min ?? 0;
2042
+ const max = opts.max ?? Infinity;
2043
+ const bad = (msg) => {
2044
+ throw new RangeError(`parseman: ${what} ${msg}`);
2045
+ };
2046
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`);
2047
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`);
2048
+ if (max < min) bad(`max (${max}) is less than min (${min}) \u2014 the combinator could never succeed`);
2049
+ return { min, max };
2050
+ }
2051
+ function many(combinator, opts = {}) {
2052
+ const { min, max } = resolveBounds("many()", opts);
2053
+ if (min >= 1) return atLeast(combinator, min, max);
1947
2054
  const meta = {
1948
2055
  firstSet: combinator._meta.firstSet,
1949
2056
  canMatchNewline: combinator._meta.canMatchNewline,
1950
2057
  isTrivia: false
1951
2058
  };
1952
- const def = { tag: "many", parser: combinator, min: 0 };
2059
+ const def = { tag: "many", parser: combinator, min: 0, ...max === Infinity ? {} : { max } };
1953
2060
  let expected;
1954
2061
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1955
2062
  return {
@@ -1959,7 +2066,9 @@ function many(combinator) {
1959
2066
  parse(input, pos, ctx) {
1960
2067
  const values = def.valueUnused ? void 0 : [];
1961
2068
  let cur = pos;
2069
+ let count = 0;
1962
2070
  while (cur < input.length) {
2071
+ if (count >= max) break;
1963
2072
  const item = repItem(combinator, input, cur, ctx, guardable);
1964
2073
  if (item === "stop") break;
1965
2074
  if ("fail" in item) {
@@ -1970,23 +2079,28 @@ function many(combinator) {
1970
2079
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1971
2080
  if (values !== void 0) values.push(error);
1972
2081
  captureError(ctx, error);
2082
+ count++;
1973
2083
  cur = end;
1974
2084
  continue;
1975
2085
  }
1976
2086
  if (values !== void 0) values.push(item.value);
2087
+ count++;
1977
2088
  cur = item.end;
1978
2089
  }
1979
2090
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1980
2091
  }
1981
2092
  };
1982
2093
  }
1983
- function oneOrMore(combinator) {
2094
+ function oneOrMore(combinator, opts = {}) {
2095
+ return many(combinator, { ...opts, min: opts.min ?? 1 });
2096
+ }
2097
+ function atLeast(combinator, min, max) {
1984
2098
  const meta = {
1985
2099
  firstSet: combinator._meta.firstSet,
1986
2100
  canMatchNewline: combinator._meta.canMatchNewline,
1987
2101
  isTrivia: false
1988
2102
  };
1989
- const def = { tag: "oneOrMore", parser: combinator, min: 1 };
2103
+ const def = { tag: "oneOrMore", parser: combinator, min, ...max === Infinity ? {} : { max } };
1990
2104
  let expected;
1991
2105
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1992
2106
  return {
@@ -1998,7 +2112,19 @@ function oneOrMore(combinator) {
1998
2112
  if (!first.ok) return first;
1999
2113
  const values = def.valueUnused ? void 0 : [first.value];
2000
2114
  let cur = first.span.end;
2115
+ let count = 1;
2116
+ while (count < min) {
2117
+ const item = repItem(combinator, input, cur, ctx, guardable);
2118
+ if (item === "stop" || "fail" in item) {
2119
+ expected ??= deriveExpected(combinator);
2120
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
2121
+ }
2122
+ if (values !== void 0) values.push(item.value);
2123
+ cur = item.end;
2124
+ count++;
2125
+ }
2001
2126
  while (cur < input.length) {
2127
+ if (count >= max) break;
2002
2128
  const item = repItem(combinator, input, cur, ctx, guardable);
2003
2129
  if (item === "stop") break;
2004
2130
  if ("fail" in item) {
@@ -2009,10 +2135,12 @@ function oneOrMore(combinator) {
2009
2135
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
2010
2136
  if (values !== void 0) values.push(error);
2011
2137
  captureError(ctx, error);
2138
+ count++;
2012
2139
  cur = end;
2013
2140
  continue;
2014
2141
  }
2015
2142
  if (values !== void 0) values.push(item.value);
2143
+ count++;
2016
2144
  cur = item.end;
2017
2145
  }
2018
2146
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
@@ -2042,17 +2170,33 @@ function optional(combinator) {
2042
2170
  }
2043
2171
  };
2044
2172
  }
2045
- function sepBy(combinator, separator) {
2173
+ function oneOrMoreSep(combinator, separator, opts = {}) {
2174
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 });
2175
+ }
2176
+ function sepBy(combinator, separator, opts = {}) {
2177
+ const { min, max } = resolveBounds("sepBy()", opts);
2178
+ const trailing = opts.trailing ?? "forbid";
2046
2179
  const meta = {
2047
2180
  firstSet: combinator._meta.firstSet,
2048
2181
  canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
2049
2182
  isTrivia: false
2050
2183
  };
2051
2184
  let expected;
2185
+ const failAt2 = (at) => {
2186
+ expected ??= deriveExpected(combinator);
2187
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } };
2188
+ };
2052
2189
  return {
2053
2190
  _tag: "sepBy",
2054
2191
  _meta: meta,
2055
- _def: { tag: "sepBy", parser: combinator, separator },
2192
+ _def: {
2193
+ tag: "sepBy",
2194
+ parser: combinator,
2195
+ separator,
2196
+ min,
2197
+ ...max === Infinity ? {} : { max },
2198
+ ...trailing === "forbid" ? {} : { trailing }
2199
+ },
2056
2200
  parse(input, pos, ctx) {
2057
2201
  const first = combinator.parse(input, pos, ctx);
2058
2202
  const values = [];
@@ -2063,6 +2207,7 @@ function sepBy(combinator, separator) {
2063
2207
  } else {
2064
2208
  const term = ctx._tolerant ? ctx._sync : void 0;
2065
2209
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2210
+ if (min >= 1) return failAt2(pos);
2066
2211
  return { ok: true, value: [], span: { start: pos, end: pos } };
2067
2212
  }
2068
2213
  expected ??= deriveExpected(combinator);
@@ -2072,6 +2217,7 @@ function sepBy(combinator, separator) {
2072
2217
  cur = rec.end;
2073
2218
  }
2074
2219
  while (cur < input.length) {
2220
+ if (values.length >= max) break;
2075
2221
  const loopMark = saveTriviaMark(ctx);
2076
2222
  let sepPos = cur;
2077
2223
  if (ctx.trivia) {
@@ -2088,6 +2234,7 @@ function sepBy(combinator, separator) {
2088
2234
  rollbackTrivia(ctx, loopMark);
2089
2235
  break;
2090
2236
  }
2237
+ const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
2091
2238
  let nextPos = sep2.span.end;
2092
2239
  if (ctx.trivia) {
2093
2240
  if (needsDeferredTriviaCommit(ctx)) {
@@ -2109,12 +2256,18 @@ function sepBy(combinator, separator) {
2109
2256
  cur = rec.end;
2110
2257
  continue;
2111
2258
  }
2259
+ if (sepMark !== void 0) {
2260
+ rollbackTrivia(ctx, sepMark);
2261
+ cur = sep2.span.end;
2262
+ break;
2263
+ }
2112
2264
  rollbackTrivia(ctx, loopMark);
2113
2265
  break;
2114
2266
  }
2115
2267
  values.push(next.value);
2116
2268
  cur = next.span.end;
2117
2269
  }
2270
+ if (values.length < min) return failAt2(cur);
2118
2271
  return { ok: true, value: values, span: { start: pos, end: cur } };
2119
2272
  }
2120
2273
  };
@@ -2167,6 +2320,7 @@ function markUnusedValues(root) {
2167
2320
  visit(def.separator, false);
2168
2321
  return;
2169
2322
  case "not":
2323
+ case "peek":
2170
2324
  visit(def.parser, false);
2171
2325
  return;
2172
2326
  case "grammar":
@@ -2285,9 +2439,9 @@ function not(combinator) {
2285
2439
  _meta: meta,
2286
2440
  _def: { tag: "not", parser: combinator },
2287
2441
  parse(input, pos, ctx) {
2288
- const mark = saveCstMark(ctx);
2442
+ const mark = saveLookaheadMark(ctx);
2289
2443
  const result = combinator.parse(input, pos, ctx);
2290
- rollbackCstCapture(ctx, mark);
2444
+ rollbackLookahead(ctx, mark);
2291
2445
  if (result.ok) {
2292
2446
  return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } };
2293
2447
  }
@@ -2296,6 +2450,28 @@ function not(combinator) {
2296
2450
  };
2297
2451
  }
2298
2452
 
2453
+ // src/combinators/peek.ts
2454
+ function peek(combinator) {
2455
+ const meta = {
2456
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
2457
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
2458
+ canMatchNewline: false,
2459
+ isTrivia: false
2460
+ };
2461
+ return {
2462
+ _tag: "peek",
2463
+ _meta: meta,
2464
+ _def: { tag: "peek", parser: combinator },
2465
+ parse(input, pos, ctx) {
2466
+ const mark = saveLookaheadMark(ctx);
2467
+ const result = combinator.parse(input, pos, ctx);
2468
+ rollbackLookahead(ctx, mark);
2469
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } };
2470
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } };
2471
+ }
2472
+ };
2473
+ }
2474
+
2299
2475
  // src/compiler/build-arity.ts
2300
2476
  var PARAM_LIST_RE = (
2301
2477
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
@@ -2394,6 +2570,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2394
2570
  case "expect":
2395
2571
  case "withCtx":
2396
2572
  case "not":
2573
+ case "peek":
2397
2574
  return parserHasOwnFields(d.parser, seen);
2398
2575
  default:
2399
2576
  return false;
@@ -2433,6 +2610,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2433
2610
  case "expect":
2434
2611
  case "withCtx":
2435
2612
  case "not":
2613
+ case "peek":
2436
2614
  case "field":
2437
2615
  return parserHasTriviaSite(d.parser, seen);
2438
2616
  case "choice":
@@ -2482,6 +2660,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2482
2660
  case "expect":
2483
2661
  case "withCtx":
2484
2662
  case "not":
2663
+ case "peek":
2485
2664
  return parserEnablesTriviaCapture(d.parser, seen);
2486
2665
  // leaf() clears the inner CST/trivia collector and publishes only its own
2487
2666
  // terminal result. An inner parser() capture cannot reach this node.
@@ -2830,7 +3009,7 @@ function leaf(root, fn) {
2830
3009
  }
2831
3010
 
2832
3011
  // src/version.ts
2833
- var PARSEMAN_VERSION = "0.32.0";
3012
+ var PARSEMAN_VERSION = "0.34.0";
2834
3013
 
2835
3014
  // src/analysis/gating.ts
2836
3015
  var isAny = (fs2) => fs2.kind === "any";
@@ -2870,14 +3049,15 @@ function firstSetToString(fs2) {
2870
3049
  var ruleNameOf = (p) => p._ruleName;
2871
3050
  var SUGGESTIONS = {
2872
3051
  "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.",
2873
- "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.",
2874
- "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.",
3052
+ "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.",
3053
+ "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.",
2875
3054
  "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.",
2876
3055
  "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2877
3056
  "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2878
3057
  };
2879
- function classifyBroadArm(arm) {
3058
+ function classifyBroadArm(arm, resolve2) {
2880
3059
  const seen = /* @__PURE__ */ new Set();
3060
+ const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
2881
3061
  const walk2 = (p) => {
2882
3062
  if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2883
3063
  seen.add(p);
@@ -2889,6 +3069,10 @@ function classifyBroadArm(arm) {
2889
3069
  return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2890
3070
  case "not":
2891
3071
  return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
3072
+ // peek() CARRIES its body's first-set, so it is only broad when the body is
3073
+ // (or the body is nullable) — walk in for the real cause.
3074
+ case "peek":
3075
+ return walk2(d.parser);
2892
3076
  case "scanTo":
2893
3077
  return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2894
3078
  // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
@@ -2905,18 +3089,23 @@ function classifyBroadArm(arm) {
2905
3089
  return walk2(d.parser);
2906
3090
  case "lazy": {
2907
3091
  const name = p._ruleName;
3092
+ let target;
2908
3093
  try {
2909
- const inner = walk2(d.thunk());
2910
- if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
2911
- return inner;
3094
+ target = d.thunk();
2912
3095
  } catch {
2913
- return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3096
+ const bound = name !== void 0 ? resolve2?.(name) : void 0;
3097
+ if (bound === void 0)
3098
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}`, unresolvedExternal: name !== void 0 };
3099
+ target = bound;
2914
3100
  }
3101
+ const inner = walk2(target);
3102
+ if (name !== void 0) return inner ? relabel(inner, "cross-artifact-ref", `via ref g.${name} \u2192 ${inner.detail}`) : null;
3103
+ return inner;
2915
3104
  }
2916
3105
  case "optional":
2917
3106
  case "many": {
2918
3107
  const inner = walk2(d.parser);
2919
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3108
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2920
3109
  }
2921
3110
  case "oneOrMore":
2922
3111
  case "transform":
@@ -2938,13 +3127,13 @@ function classifyBroadArm(arm) {
2938
3127
  sawNullablePrefix = true;
2939
3128
  continue;
2940
3129
  }
2941
- if (isAny(firstSetOf(t))) {
3130
+ if (isAny(firstSetOf(t, /* @__PURE__ */ new Set(), resolve2))) {
2942
3131
  const inner = walk2(t);
2943
3132
  if (sawNullablePrefix)
2944
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3133
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2945
3134
  return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2946
3135
  }
2947
- if (matchesEmpty(t)) {
3136
+ if (matchesEmpty(t, /* @__PURE__ */ new Set(), resolve2)) {
2948
3137
  sawNullablePrefix = true;
2949
3138
  continue;
2950
3139
  }
@@ -2955,7 +3144,7 @@ function classifyBroadArm(arm) {
2955
3144
  case "choice": {
2956
3145
  for (const a of d.parsers) {
2957
3146
  const r = walk2(a);
2958
- if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3147
+ if (r) return relabel(r, r.cause, `choice arm \u2192 ${r.detail}`);
2959
3148
  }
2960
3149
  return null;
2961
3150
  }
@@ -2992,7 +3181,7 @@ function detectAntiPatterns(rule, arms) {
2992
3181
  kind: "double-not",
2993
3182
  rule,
2994
3183
  armIndex: i,
2995
- 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."
3184
+ 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."
2996
3185
  });
2997
3186
  } else {
2998
3187
  out.push({
@@ -3015,6 +3204,9 @@ function detectAntiPatterns(rule, arms) {
3015
3204
  return out;
3016
3205
  }
3017
3206
  function analyzeGating(entry, opts) {
3207
+ return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3208
+ }
3209
+ function analyzeGatingRules(ruleMap, opts) {
3018
3210
  const raw = [];
3019
3211
  const antiPatterns = [];
3020
3212
  const visited = /* @__PURE__ */ new Set();
@@ -3024,7 +3216,7 @@ function analyzeGating(entry, opts) {
3024
3216
  const d = p._def;
3025
3217
  const rule = ruleNameOf(p) ?? enclosingRule;
3026
3218
  if (d.tag === "choice") {
3027
- raw.push({ g: analyzeChoice(p, d, rule), rule });
3219
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
3028
3220
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3029
3221
  }
3030
3222
  const rec = d;
@@ -3041,7 +3233,7 @@ function analyzeGating(entry, opts) {
3041
3233
  }
3042
3234
  for (const k of kids) visit(k, rule);
3043
3235
  };
3044
- visit(entry, ruleNameOf(entry) ?? "<entry>");
3236
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3045
3237
  const perRule = /* @__PURE__ */ new Map();
3046
3238
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3047
3239
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3057,18 +3249,19 @@ function analyzeGating(entry, opts) {
3057
3249
  });
3058
3250
  const gated = choices.filter((c) => c.gates === "yes").length;
3059
3251
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3060
- const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3252
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted && !c.deferred);
3061
3253
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3254
+ const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3062
3255
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3063
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3256
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3064
3257
  }
3065
- function analyzeChoice(p, d, rule) {
3258
+ function analyzeChoice(p, d, rule, resolve2) {
3066
3259
  const arms = d.parsers;
3067
3260
  const shallow = arms.map((a) => a._meta.firstSet);
3068
- const deep = arms.map((a) => firstSetOf(a));
3261
+ const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
3069
3262
  const anyArms = deep.map((fs2, index) => ({ fs: fs2, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3070
- const { cause, detail } = classifyBroadArm(arms[index]);
3071
- return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3263
+ const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve2);
3264
+ return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
3072
3265
  });
3073
3266
  const overlaps = [];
3074
3267
  for (let i = 0; i < deep.length; i++)
@@ -3084,10 +3277,12 @@ function analyzeChoice(p, d, rule) {
3084
3277
  if (d.disjoint) gates = "yes";
3085
3278
  else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3086
3279
  else gates = "no";
3280
+ const deferred = gates === "no" && overlaps.length === 0 && anyArms.length > 0 && anyArms.every((a) => a.unresolvedExternal);
3087
3281
  return {
3088
3282
  rule,
3089
3283
  strategy: d.strategy?.tag ?? "firstMatch",
3090
3284
  gates,
3285
+ deferred,
3091
3286
  combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3092
3287
  anyArms,
3093
3288
  overlaps
@@ -3254,6 +3449,7 @@ function children(def, winners) {
3254
3449
  case "field":
3255
3450
  case "grammar":
3256
3451
  case "not":
3452
+ case "peek":
3257
3453
  case "node":
3258
3454
  case "guard":
3259
3455
  case "withCtx":
@@ -5027,6 +5223,9 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
5027
5223
  case "keywords":
5028
5224
  case "guard":
5029
5225
  case "not":
5226
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
5227
+ // outcomes, so it can never leave a partial capture behind.
5228
+ case "peek":
5030
5229
  case "trivia":
5031
5230
  case "token":
5032
5231
  case "leaf":
@@ -5098,6 +5297,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
5098
5297
  case "leaf":
5099
5298
  return true;
5100
5299
  case "not":
5300
+ case "peek":
5101
5301
  case "guard":
5102
5302
  case "trivia":
5103
5303
  case "unknown":
@@ -5224,6 +5424,7 @@ function emitKeywordsFast(def, ctx, pos) {
5224
5424
  if (def.words.length === 0 || def.words.some((w) => w.length === 0)) return null;
5225
5425
  if (def.words.some((w) => Array.from(w).length !== w.length)) return null;
5226
5426
  if (def.caseInsensitive && def.boundary) return null;
5427
+ if (def.caseInsensitive && def.words.some((w) => w.split("").some((c) => c.charCodeAt(0) > 127))) return null;
5227
5428
  let boundary = null;
5228
5429
  if (def.boundary) {
5229
5430
  let body = def.boundary;
@@ -5271,7 +5472,7 @@ function emitKeywords(def, ctx, pos) {
5271
5472
  if (fast) return fast;
5272
5473
  const alt = def.words.map(escapeKeywordRe).join("|");
5273
5474
  const boundary = def.boundary ? `(?![${def.boundary}])` : "";
5274
- const flags = def.caseInsensitive ? "iuy" : "uy";
5475
+ const flags = def.caseInsensitive ? "iy" : "uy";
5275
5476
  const source = `(?:${alt})${boundary}`;
5276
5477
  const key = `${source}/${flags}`;
5277
5478
  let rName = ctx.regexMap.get(key);
@@ -5417,6 +5618,7 @@ function failsAtStart(p) {
5417
5618
  case "keywords":
5418
5619
  case "guard":
5419
5620
  case "not":
5621
+ case "peek":
5420
5622
  return true;
5421
5623
  case "transform":
5422
5624
  case "label":
@@ -5830,14 +6032,17 @@ function emitMany(def, ctx, pos) {
5830
6032
  ];
5831
6033
  const mySyncV = ctx.recovery ? v(ctx, "_mysy") : "";
5832
6034
  if (ctx.recovery) stmts.push(`${ind(ctx)}const ${mySyncV} = _ctx._sync`);
5833
- if (def.min === 1) {
6035
+ for (let i = 0; i < def.min; i++) {
5834
6036
  const firstR = emit(def.parser, ctx, curV);
5835
6037
  stmts.push(...firstR.stmts);
5836
6038
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
5837
6039
  stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
5838
6040
  }
6041
+ const maxV = def.max === void 0 ? null : wantValue ? `${arrV}.length` : v(ctx, "_cnt");
6042
+ if (maxV !== null && !wantValue) stmts.push(`${ind(ctx)}let ${maxV} = ${def.min}`);
5839
6043
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
5840
6044
  ctx.indent++;
6045
+ if (maxV !== null) stmts.push(`${ind(ctx)}if (${maxV} >= ${def.max}) break`);
5841
6046
  let itemPos = curV;
5842
6047
  let rollback = "";
5843
6048
  if (ctx.activeTrivia) {
@@ -5878,7 +6083,7 @@ function emitMany(def, ctx, pos) {
5878
6083
  `${ind(ctx)}if (!${iterOk}) {`,
5879
6084
  `${ind(ctx)} ${rollback}if (_ctx._tolerant && ${mySyncV} !== undefined && !_ctx._rec.at(${mySyncV}, input, ${itemPos}, _ctx)) {`,
5880
6085
  `${ind(ctx)} const ${rrV} = _ctx._rec.scan(input, ${itemPos}, _ctx, ${mySyncV}, ${exp})`,
5881
- ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : [],
6086
+ ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : maxV !== null ? [`${ind(ctx)} ${maxV}++`] : [],
5882
6087
  `${ind(ctx)} _ctx._rec.capture(_ctx, ${rrV}.error)`,
5883
6088
  `${ind(ctx)} ${curV} = ${rrV}.end`,
5884
6089
  `${ind(ctx)} continue`,
@@ -5891,6 +6096,7 @@ function emitMany(def, ctx, pos) {
5891
6096
  stmts.push(`${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`);
5892
6097
  }
5893
6098
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
6099
+ else if (maxV !== null) stmts.push(`${ind(ctx)}${maxV}++`);
5894
6100
  stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
5895
6101
  ctx.indent--;
5896
6102
  stmts.push(`${ind(ctx)}}`);
@@ -5955,8 +6161,8 @@ function emitSepBy(_p, def, ctx, pos) {
5955
6161
  ...rec ? [`${ind(ctx)}const ${mySyncV} = _ctx._sync`] : [],
5956
6162
  ...firstStmts
5957
6163
  ];
5958
- const failItem = (nextOkVar, itemPosVar, rb, originalFail) => {
5959
- if (!rec) return [originalFail];
6164
+ const failItem = (nextOkVar, itemPosVar, breakStmt) => {
6165
+ if (!rec) return [`${ind(ctx)}if (!${nextOkVar}) ${breakStmt === "break" ? "break" : `{ ${breakStmt} }`}`];
5960
6166
  const rr = v(ctx, "_rr");
5961
6167
  return [
5962
6168
  `${ind(ctx)}if (!${nextOkVar}) {`,
@@ -5964,10 +6170,24 @@ function emitSepBy(_p, def, ctx, pos) {
5964
6170
  `${ind(ctx)} const ${rr} = _ctx._rec.scan(input, ${itemPosVar}, _ctx, ${scanSync}, ${exp})`,
5965
6171
  `${ind(ctx)} ${arrV}.push(${rr}.error); _ctx._rec.capture(_ctx, ${rr}.error); ${curV} = ${rr}.end; continue`,
5966
6172
  `${ind(ctx)} }`,
5967
- `${ind(ctx)} ${rb}break`,
6173
+ `${ind(ctx)} ${breakStmt}`,
5968
6174
  `${ind(ctx)}}`
5969
6175
  ];
5970
6176
  };
6177
+ const itemFailBreak = (sepEndVar, sepRb, postSepRb) => def.trailing === void 0 ? `${sepRb}break` : `${postSepRb}${curV} = ${sepEndVar}; break`;
6178
+ const postSepMarks = () => {
6179
+ if (def.trailing === void 0 || !ctx.capturing) return { decl: [], rb: "" };
6180
+ const lv = v(ctx, "_tlv"), rw = v(ctx, "_trw"), tl = v(ctx, "_ttl"), lg = v(ctx, "_tlg");
6181
+ return {
6182
+ decl: [
6183
+ `${ind(ctx)}const ${lv} = _ctx._cstLeaves ? _ctx._cstLeaves.length : 0`,
6184
+ `${ind(ctx)}const ${rw} = _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0`,
6185
+ `${ind(ctx)}const ${tl} = _ctx._cstTriviaLog ? _ctx._cstTriviaLog.length : 0`,
6186
+ `${ind(ctx)}const ${lg} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`
6187
+ ],
6188
+ rb: `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${lv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${rw}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${tl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${lg}; `
6189
+ };
6190
+ };
5971
6191
  if (rec) {
5972
6192
  const rr0 = v(ctx, "_rr0");
5973
6193
  const didV = v(ctx, "_did");
@@ -5993,6 +6213,7 @@ function emitSepBy(_p, def, ctx, pos) {
5993
6213
  );
5994
6214
  ctx.indent++;
5995
6215
  }
6216
+ if (def.max !== void 0) stmts.push(`${ind(ctx)}if (${arrV}.length >= ${def.max}) break`);
5996
6217
  let sepAtPos = curV;
5997
6218
  if (ctx.activeTrivia) {
5998
6219
  if (ctx.capturing) {
@@ -6013,12 +6234,14 @@ function emitSepBy(_p, def, ctx, pos) {
6013
6234
  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}; `;
6014
6235
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6015
6236
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) { ${rollbackToSep}break }`);
6237
+ const post = postSepMarks();
6238
+ stmts.push(...post.decl);
6016
6239
  const npV = v(ctx, "_np");
6017
6240
  stmts.push(`${ind(ctx)}const ${npV} = ${capFn}(input, ${sepEnd}, _ctx, 1)`);
6018
6241
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6019
6242
  stmts.push(
6020
6243
  ...nextStmts,
6021
- ...failItem(nextOk, npV, rollbackToSep, `${ind(ctx)}if (!${nextOk}) { ${rollbackToSep}break }`),
6244
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, rollbackToSep, post.rb)),
6022
6245
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6023
6246
  `${ind(ctx)}${curV} = ${nextEnd}`
6024
6247
  );
@@ -6034,7 +6257,7 @@ function emitSepBy(_p, def, ctx, pos) {
6034
6257
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6035
6258
  stmts.push(
6036
6259
  ...nextStmts,
6037
- ...failItem(nextOk, npV, "", `${ind(ctx)}if (!${nextOk}) break`),
6260
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, "", "")),
6038
6261
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6039
6262
  `${ind(ctx)}${curV} = ${nextEnd}`
6040
6263
  );
@@ -6051,10 +6274,12 @@ function emitSepBy(_p, def, ctx, pos) {
6051
6274
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6052
6275
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) break`);
6053
6276
  const nextRb = markLv ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6277
+ const post = postSepMarks();
6278
+ stmts.push(...post.decl);
6054
6279
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, sepEnd, true);
6055
6280
  stmts.push(
6056
6281
  ...nextStmts,
6057
- ...failItem(nextOk, sepEnd, nextRb, `${ind(ctx)}if (!${nextOk}) { ${nextRb}break }`),
6282
+ ...failItem(nextOk, sepEnd, itemFailBreak(sepEnd, nextRb, post.rb)),
6058
6283
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6059
6284
  `${ind(ctx)}${curV} = ${nextEnd}`
6060
6285
  );
@@ -6063,6 +6288,9 @@ function emitSepBy(_p, def, ctx, pos) {
6063
6288
  stmts.push(`${ind(ctx)}}`);
6064
6289
  ctx.indent--;
6065
6290
  stmts.push(`${ind(ctx)}}`);
6291
+ if (def.min >= 1) {
6292
+ stmts.push(...emitIfFail(ctx, `${arrV}.length < ${def.min}`, failArrBody(ctx, deriveExpectedArr([def.parser]), curV)));
6293
+ }
6066
6294
  return { stmts, valueVar: arrV, endVar: curV };
6067
6295
  }
6068
6296
  function emitScanTo(def, ctx, pos) {
@@ -6114,17 +6342,44 @@ function emitScanTo(def, ctx, pos) {
6114
6342
  return { stmts, valueVar: valV, endVar: curV };
6115
6343
  }
6116
6344
  function emitNot(def, ctx, pos) {
6345
+ const sinksLive = !!ctx.capturing || !!ctx.recovery;
6346
+ const leaves = sinksLive ? v(ctx, "_ntl") : null;
6347
+ const raw = sinksLive ? v(ctx, "_ntr") : null;
6348
+ const tl = sinksLive ? v(ctx, "_ntt") : null;
6349
+ const log = sinksLive ? v(ctx, "_ntg") : null;
6350
+ const fields = sinksLive ? v(ctx, "_ntf") : null;
6351
+ const errors = sinksLive ? v(ctx, "_nte") : null;
6117
6352
  const { stmts, okVar } = emitFallible(def.parser, ctx, pos, true);
6118
6353
  const label2 = JSON.stringify(`not(${def.parser._tag})`);
6119
6354
  return {
6120
6355
  stmts: [
6356
+ ...sinksLive ? [
6357
+ `${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`
6358
+ ] : [],
6121
6359
  ...stmts,
6360
+ ...sinksLive ? [
6361
+ `${ind(ctx)}if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${leaves}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${raw}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${tl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${log}; if (_ctx._fields) _ctx._fields.length = ${fields}; if (_ctx._errors) _ctx._errors.length = ${errors}`
6362
+ ] : [],
6122
6363
  ...emitIfFail(ctx, okVar, failBody(ctx, label2, pos))
6123
6364
  ],
6124
6365
  valueVar: "null",
6125
6366
  endVar: pos
6126
6367
  };
6127
6368
  }
6369
+ function emitPeek(def, ctx, pos) {
6370
+ const probeCtx = { ...ctx, capturing: false, noHoist: true };
6371
+ const { stmts, okVar } = emitFallible(def.parser, probeCtx, pos, true);
6372
+ ctx.vars = probeCtx.vars;
6373
+ const label2 = JSON.stringify(`peek(${def.parser._tag})`);
6374
+ return {
6375
+ stmts: [
6376
+ ...stmts,
6377
+ ...emitIfFail(ctx, `!${okVar}`, failBody(ctx, label2, pos))
6378
+ ],
6379
+ valueVar: "null",
6380
+ endVar: pos
6381
+ };
6382
+ }
6128
6383
  function escapeTokenLiteral(value) {
6129
6384
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6130
6385
  }
@@ -6412,9 +6667,9 @@ function emitRuntimeFallback(parser2, ctx, pos) {
6412
6667
  return { stmts, valueVar: vv, endVar: ev };
6413
6668
  }
6414
6669
  function emitLazy(p, def, ctx, pos) {
6415
- const canonical = ctx.ruleNames?.get(p);
6670
+ const canonical2 = ctx.ruleNames?.get(p);
6416
6671
  const usage = ctx.lazyUsage;
6417
- if (!canonical && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6672
+ if (!canonical2 && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6418
6673
  let resolved;
6419
6674
  try {
6420
6675
  resolved = def.thunk();
@@ -6424,7 +6679,7 @@ function emitLazy(p, def, ctx, pos) {
6424
6679
  return emit(resolved, ctx, pos);
6425
6680
  }
6426
6681
  if (!ctx.namedParsers.has(p)) {
6427
- const fnName2 = canonical ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6682
+ const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6428
6683
  ctx.namedParsers.set(p, fnName2);
6429
6684
  let resolved;
6430
6685
  try {
@@ -6707,6 +6962,8 @@ function emitDispatch(p, ctx, pos) {
6707
6962
  }
6708
6963
  case "not":
6709
6964
  return emitNot(def, ctx, pos);
6965
+ case "peek":
6966
+ return emitPeek(def, ctx, pos);
6710
6967
  case "node":
6711
6968
  return emitNode(def, ctx, pos);
6712
6969
  case "scanTo":
@@ -6795,6 +7052,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
6795
7052
  case "many":
6796
7053
  case "oneOrMore":
6797
7054
  case "not":
7055
+ case "peek":
6798
7056
  case "transform":
6799
7057
  return hasNodeDef(d.parser, seen);
6800
7058
  case "skip":
@@ -6846,17 +7104,18 @@ function childrenOf(def) {
6846
7104
  case "field":
6847
7105
  case "grammar":
6848
7106
  case "not":
7107
+ case "peek":
6849
7108
  case "node":
6850
7109
  case "withCtx":
6851
7110
  case "expect":
6852
7111
  return [def.parser];
6853
- // emitMany's oneOrMore branch and emitSepBy both codegen `def.parser` TWICE
6854
- // (a mandatory first match, then again inside the repeat loop) — two real
6855
- // emit() call sites, so the usage analysis must see two edges here or it
7112
+ // emitMany's min>=1 branch and emitSepBy both codegen `def.parser` more than
7113
+ // once (`min` mandatory matches, then again inside the repeat loop) — each is a
7114
+ // real emit() call site, so the usage analysis must see one edge per site or it
6856
7115
  // undercounts a single-use `parser` ref as inline-safe when it's actually
6857
- // referenced from two positions within this one compiled function.
7116
+ // referenced from several positions within this one compiled function.
6858
7117
  case "oneOrMore":
6859
- return [def.parser, def.parser];
7118
+ return Array.from({ length: def.min + 1 }, () => def.parser);
6860
7119
  case "sepBy":
6861
7120
  return [def.parser, def.parser, def.separator];
6862
7121
  case "skip":
@@ -7002,12 +7261,41 @@ function resolveGatingLevel(opt) {
7002
7261
  return "warn";
7003
7262
  }
7004
7263
  function runGatingDiagnostic(combinator, opt) {
7264
+ return reportGating(opt, (o) => analyzeGating(combinator, o));
7265
+ }
7266
+ function runGatingDiagnosticRules(ruleMap, opt) {
7267
+ return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7268
+ }
7269
+ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7270
+ if (resolveGatingLevel(opt) === "off") return void 0;
7271
+ const ruleMaps = getRuleMaps();
7272
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
7273
+ const winners = /* @__PURE__ */ new Map();
7274
+ for (const map of ruleMaps) for (const [name, rule] of map) {
7275
+ if (rule._def.tag === "lazy") {
7276
+ try {
7277
+ rule._def.thunk();
7278
+ } catch {
7279
+ continue;
7280
+ }
7281
+ }
7282
+ winners.set(name, rule);
7283
+ }
7284
+ const entries = [...winners];
7285
+ return reportGating(opt, (o) => {
7286
+ const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7287
+ const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7288
+ return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
7289
+ });
7290
+ }
7291
+ function reportGating(opt, analyze) {
7005
7292
  const level = resolveGatingLevel(opt);
7006
7293
  if (level === "off") return void 0;
7007
- const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7294
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
7295
+ 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;
7008
7296
  let report;
7009
7297
  try {
7010
- report = analyzeGating(combinator, accept ? { accept } : void 0);
7298
+ report = analyze(analyzeOpts);
7011
7299
  } catch {
7012
7300
  return void 0;
7013
7301
  }
@@ -7143,6 +7431,7 @@ function publicRuleWrapperSource(rule, fnSource) {
7143
7431
  return `Object.assign(${fnSource}, { _meta: { triviaKindLabels: ${JSON.stringify(labels)} } })`;
7144
7432
  }
7145
7433
  function compileRuleMap(ruleMap, opts) {
7434
+ runGatingDiagnosticRules(ruleMap, opts?.gating);
7146
7435
  for (const [, rule] of ruleMap) markUnusedValues(rule);
7147
7436
  const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
7148
7437
  const grammarTrivia = opts?.trivia;
@@ -7261,6 +7550,7 @@ function compileRuleMap(ruleMap, opts) {
7261
7550
  }
7262
7551
  function compileLinkable(ruleMapArg, ns, opts) {
7263
7552
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7553
+ if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
7264
7554
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7265
7555
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7266
7556
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
@@ -7317,6 +7607,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7317
7607
  fnNameToKey.set(fn, key);
7318
7608
  }
7319
7609
  ctx.ruleNames = ruleNames;
7610
+ const externalRefs = /* @__PURE__ */ new Set();
7320
7611
  {
7321
7612
  const scanned = /* @__PURE__ */ new Set();
7322
7613
  const scanExternal = (p) => {
@@ -7332,11 +7623,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
7332
7623
  }
7333
7624
  if (resolved === void 0) {
7334
7625
  const name = p._ruleName;
7335
- if (name && !ctx.namedParsers.has(p)) {
7336
- assertRuleName(name);
7337
- const fn = `_r_${name}`;
7338
- ruleNames.set(p, fn);
7339
- ctx.namedParsers.set(p, fn);
7626
+ if (name) {
7627
+ externalRefs.add(p);
7628
+ if (!ctx.namedParsers.has(p)) {
7629
+ assertRuleName(name);
7630
+ const fn = `_r_${name}`;
7631
+ ruleNames.set(p, fn);
7632
+ ctx.namedParsers.set(p, fn);
7633
+ }
7340
7634
  }
7341
7635
  return;
7342
7636
  }
@@ -7449,12 +7743,32 @@ function compileLinkable(ruleMapArg, ns, opts) {
7449
7743
  needsEmptyTl: !!ctx.needsEmptyTl,
7450
7744
  needsHostReads: !!ctx.needsHostReads,
7451
7745
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7452
- isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule)),
7746
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7453
7747
  mfFns: mfSrcs ? [] : ctx.mapFns,
7454
7748
  buildFns: buildSrcs ? [] : ctx.buildFns
7455
7749
  };
7456
7750
  }
7457
- function hasSemanticReduction(roots) {
7751
+ function hasExternalRuleRef(ruleMap) {
7752
+ const seen = /* @__PURE__ */ new Set();
7753
+ const visit = (p) => {
7754
+ if (seen.has(p)) return false;
7755
+ seen.add(p);
7756
+ const def = p._def;
7757
+ if (def.tag === "lazy") {
7758
+ let resolved;
7759
+ try {
7760
+ resolved = def.thunk();
7761
+ } catch {
7762
+ resolved = void 0;
7763
+ }
7764
+ if (resolved === void 0) return p._ruleName !== void 0;
7765
+ return visit(resolved);
7766
+ }
7767
+ return childrenOf(def).some(visit);
7768
+ };
7769
+ return ruleMap.some(([, rule]) => visit(rule));
7770
+ }
7771
+ function hasSemanticReduction(roots, externalRefs) {
7458
7772
  const seen = /* @__PURE__ */ new Set();
7459
7773
  const visit = (parser2) => {
7460
7774
  if (seen.has(parser2)) return false;
@@ -7465,6 +7779,7 @@ function hasSemanticReduction(roots) {
7465
7779
  if (def.tag === "guard" || def.tag === "withCtx") return true;
7466
7780
  if (def.tag === "node" && def.build !== void 0) return true;
7467
7781
  if (def.tag === "lazy") {
7782
+ if (externalRefs?.has(parser2)) return false;
7468
7783
  try {
7469
7784
  return visit(def.thunk());
7470
7785
  } catch {
@@ -7556,6 +7871,7 @@ function childrenOf2(def) {
7556
7871
  case "label":
7557
7872
  case "field":
7558
7873
  case "not":
7874
+ case "peek":
7559
7875
  case "node":
7560
7876
  case "withCtx":
7561
7877
  case "expect":
@@ -7589,9 +7905,16 @@ function lazyTarget(c) {
7589
7905
  }
7590
7906
  var Unserializable = class extends Error {
7591
7907
  };
7592
- function serializeRuleMap(ruleMap) {
7908
+ function repeatOpts(min, max, trailing) {
7909
+ const parts = [];
7910
+ if (min !== 0) parts.push(`min: ${min}`);
7911
+ if (max !== void 0) parts.push(`max: ${max}`);
7912
+ if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
7913
+ return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
7914
+ }
7915
+ function serializeRuleMap(ruleMap, scanSkip) {
7593
7916
  try {
7594
- return new Serializer(ruleMap).run();
7917
+ return new Serializer(ruleMap, scanSkip ?? []).run();
7595
7918
  } catch (e) {
7596
7919
  if (e instanceof Unserializable) {
7597
7920
  if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${e.message}`);
@@ -7692,6 +8015,7 @@ function evalRuleMapIR(ir) {
7692
8015
  "optional",
7693
8016
  "sepBy",
7694
8017
  "not",
8018
+ "peek",
7695
8019
  "node",
7696
8020
  "parser",
7697
8021
  "scanTo",
@@ -7724,6 +8048,7 @@ function evalRuleMapIR(ir) {
7724
8048
  optional,
7725
8049
  sepBy,
7726
8050
  not,
8051
+ peek,
7727
8052
  node,
7728
8053
  parser,
7729
8054
  scanTo,
@@ -7752,10 +8077,28 @@ var Serializer = class {
7752
8077
  decls = [];
7753
8078
  ruleValues = /* @__PURE__ */ new Set();
7754
8079
  ruleMap;
7755
- constructor(ruleMap) {
8080
+ scanSkip;
8081
+ constructor(ruleMap, scanSkip = []) {
7756
8082
  this.ruleMap = ruleMap;
8083
+ this.scanSkip = scanSkip;
7757
8084
  for (const [, c] of ruleMap) this.ruleValues.add(this.body(c));
7758
8085
  }
8086
+ /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
8087
+ * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
8088
+ * serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
8089
+ * outside the `(g) => …` factory where `g` is bound. A unit that does reference a
8090
+ * rule can't be carried in options; bail to the full-pieces fallback. */
8091
+ assertNoRuleRef(c, seen) {
8092
+ if (seen.has(c)) return;
8093
+ seen.add(c);
8094
+ if (c._def.tag === "lazy") {
8095
+ if (ruleNameOf2(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
8096
+ const target = lazyTarget(c);
8097
+ if (target) this.assertNoRuleRef(target, seen);
8098
+ return;
8099
+ }
8100
+ for (const child of childrenOf2(c._def)) this.assertNoRuleRef(child, seen);
8101
+ }
7759
8102
  /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
7760
8103
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7761
8104
  * `g[name]` ref — only the top-level entry is unwrapped. */
@@ -7764,21 +8107,38 @@ var Serializer = class {
7764
8107
  }
7765
8108
  run() {
7766
8109
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
8110
+ for (const c of this.scanSkip) {
8111
+ this.assertNoRuleRef(c, /* @__PURE__ */ new Set());
8112
+ this.analyze(c, /* @__PURE__ */ new Set());
8113
+ }
7767
8114
  let n = 0;
7768
8115
  for (const [c, count] of this.counts) {
7769
8116
  if (this.ruleValues.has(c)) continue;
7770
8117
  if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`);
7771
8118
  }
8119
+ const outerDecls = [];
8120
+ const innerDecls = [];
8121
+ let optionsArg = "";
8122
+ if (this.scanSkip.length > 0) {
8123
+ this.decls = outerDecls;
8124
+ const units = this.scanSkip.map((c) => this.ref(c, null));
8125
+ optionsArg = `{ scanSkip: [${units.join(", ")}] }, `;
8126
+ }
8127
+ this.decls = innerDecls;
7772
8128
  const body = this.ruleMap.map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c))}`).join(",\n");
7773
- if (this.decls.length === 0) return `rules((g) => ({
8129
+ const factory = innerDecls.length === 0 ? `(g) => ({
7774
8130
  ${body}
7775
- }))`;
7776
- return `rules((g) => {
7777
- ${this.decls.map((d) => " " + d).join("\n")}
8131
+ })` : `(g) => {
8132
+ ${innerDecls.map((d) => " " + d).join("\n")}
7778
8133
  return ({
7779
8134
  ${body}
7780
8135
  })
7781
- })`;
8136
+ }`;
8137
+ const call = `rules(${optionsArg}${factory})`;
8138
+ return outerDecls.length === 0 ? call : `(() => {
8139
+ ${outerDecls.map((d) => " " + d).join("\n")}
8140
+ return ${call}
8141
+ })()`;
7782
8142
  }
7783
8143
  /** Count identity references and flag self-referential subtrees. `active` is the
7784
8144
  * set of combinators on the current DFS path (to detect a lazy pointing back). */
@@ -7863,17 +8223,21 @@ ${body}
7863
8223
  return `_gch([${items.join(", ")}])`;
7864
8224
  }
7865
8225
  case "many":
7866
- return `many(${kid(def.parser)})`;
8226
+ return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
8227
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
8228
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
7867
8229
  case "oneOrMore":
7868
- return `oneOrMore(${kid(def.parser)})`;
8230
+ return def.min === 1 && def.max === void 0 ? `oneOrMore(${kid(def.parser)})` : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
7869
8231
  case "optional":
7870
8232
  return `optional(${kid(def.parser)})`;
7871
8233
  case "attempt":
7872
8234
  return `attempt(${kid(def.parser)})`;
7873
8235
  case "sepBy":
7874
- return `sepBy(${kid(def.parser)}, ${kid(def.separator)})`;
8236
+ return `sepBy(${kid(def.parser)}, ${kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`;
7875
8237
  case "not":
7876
8238
  return `not(${kid(def.parser)})`;
8239
+ case "peek":
8240
+ return `peek(${kid(def.parser)})`;
7877
8241
  case "trivia":
7878
8242
  return `trivia(${kid(def.parser)})`;
7879
8243
  case "token":
@@ -8309,15 +8673,12 @@ var SUPPORTED = {
8309
8673
  literal: (...a) => literal(a[0], a[1]),
8310
8674
  regex: (...a) => regex(a[0], a[1]),
8311
8675
  keywords: (...a) => keywords(a[0], a[1]),
8312
- word: (...a) => word(a[0], a[1]),
8676
+ word: (...a) => word(a[0], a[1], a[2]),
8313
8677
  sequence: (...a) => sequence(...a),
8314
8678
  choice: (...a) => choice(...a),
8315
8679
  attempt: (...a) => attempt(a[0]),
8316
- many: (...a) => many(a[0]),
8317
- oneOrMore: (...a) => oneOrMore(a[0]),
8318
8680
  optional: (...a) => optional(a[0]),
8319
8681
  skip: (...a) => skip(a[0], a[1]),
8320
- sepBy: (...a) => sepBy(a[0], a[1]),
8321
8682
  trivia: (...a) => trivia(a[0]),
8322
8683
  label: (...a) => label(a[0], a[1]),
8323
8684
  field: (...a) => field(a[0], a[1]),
@@ -8493,8 +8854,8 @@ function exprToCombi(node2, scope, code, mfs) {
8493
8854
  return null;
8494
8855
  }
8495
8856
  }
8496
- if (callee.name === "sepBy") {
8497
- const [itemArg, sepArg] = node2.arguments;
8857
+ if (callee.name === "sepBy" || callee.name === "oneOrMoreSep") {
8858
+ const [itemArg, sepArg, optsArg] = node2.arguments;
8498
8859
  if (!itemArg || !sepArg || itemArg.type === "SpreadElement" || sepArg.type === "SpreadElement") return null;
8499
8860
  const itemMfs = [];
8500
8861
  const itemCombi = anyValue(itemArg, scope, code, itemMfs);
@@ -8502,25 +8863,42 @@ function exprToCombi(node2, scope, code, mfs) {
8502
8863
  const sepMfs = [];
8503
8864
  const sepCombi = anyValue(sepArg, scope, code, sepMfs);
8504
8865
  if (!isCombinator(sepCombi)) return null;
8866
+ let opts;
8867
+ if (optsArg) {
8868
+ if (optsArg.type === "SpreadElement") return null;
8869
+ const v2 = anyValue(optsArg, scope, code, []);
8870
+ if (!v2 || typeof v2 !== "object") return null;
8871
+ opts = v2;
8872
+ }
8505
8873
  if (mfs) mfs.push(...itemMfs, ...sepMfs, ...itemMfs);
8506
8874
  try {
8507
- return sepBy(itemCombi, sepCombi);
8875
+ return callee.name === "sepBy" ? sepBy(itemCombi, sepCombi, opts) : oneOrMoreSep(itemCombi, sepCombi, opts);
8508
8876
  } catch {
8509
8877
  return null;
8510
8878
  }
8511
8879
  }
8512
- if (callee.name === "oneOrMore") {
8513
- const [itemArg] = node2.arguments;
8880
+ if (callee.name === "oneOrMore" || callee.name === "many") {
8881
+ const [itemArg, optsArg] = node2.arguments;
8514
8882
  if (!itemArg || itemArg.type === "SpreadElement") return null;
8515
8883
  const itemMfs = [];
8516
8884
  const itemCombi = anyValue(itemArg, scope, code, itemMfs);
8517
8885
  if (!isCombinator(itemCombi)) return null;
8518
- if (mfs) mfs.push(...itemMfs, ...itemMfs);
8886
+ let opts;
8887
+ if (optsArg) {
8888
+ if (optsArg.type === "SpreadElement") return null;
8889
+ const v2 = anyValue(optsArg, scope, code, []);
8890
+ if (!v2 || typeof v2 !== "object") return null;
8891
+ opts = v2;
8892
+ }
8893
+ let combi;
8519
8894
  try {
8520
- return oneOrMore(itemCombi);
8895
+ combi = callee.name === "many" ? many(itemCombi, opts) : oneOrMore(itemCombi, opts);
8521
8896
  } catch {
8522
8897
  return null;
8523
8898
  }
8899
+ const min = combi._def.tag === "oneOrMore" ? combi._def.min : 0;
8900
+ if (mfs) for (let i = 0; i <= min; i++) mfs.push(...itemMfs);
8901
+ return combi;
8524
8902
  }
8525
8903
  if (callee.name === "not") {
8526
8904
  const [innerArg] = node2.arguments;
@@ -8533,6 +8911,17 @@ function exprToCombi(node2, scope, code, mfs) {
8533
8911
  return null;
8534
8912
  }
8535
8913
  }
8914
+ if (callee.name === "peek") {
8915
+ const [innerArg] = node2.arguments;
8916
+ if (!innerArg || innerArg.type === "SpreadElement") return null;
8917
+ const inner = anyValue(innerArg, scope, code, mfs);
8918
+ if (!isCombinator(inner)) return null;
8919
+ try {
8920
+ return peek(inner);
8921
+ } catch {
8922
+ return null;
8923
+ }
8924
+ }
8536
8925
  if (callee.name === "balanced") {
8537
8926
  const [openArg, closeArg, optsArg] = node2.arguments;
8538
8927
  if (!openArg || !closeArg || openArg.type === "SpreadElement" || closeArg.type === "SpreadElement") return null;
@@ -9162,6 +9551,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9162
9551
  const warnings = [];
9163
9552
  beginLoweringCapture();
9164
9553
  let anyUnresolved = false;
9554
+ let keepMacroImport = false;
9165
9555
  let runtimeComposeFallback = false;
9166
9556
  let ruleMapHolderCounter = 0;
9167
9557
  const lineOf = (pos) => {
@@ -9196,17 +9586,21 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9196
9586
  const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
9197
9587
  const scanSkipValue = optionValue("scanSkip");
9198
9588
  const gScanSkip = scanSkipValue ? evaluateCombinatorArray(scanSkipValue, scope, code) ?? void 0 : void 0;
9589
+ if (gScanSkip) {
9590
+ for (const rule of ruleMap.values()) {
9591
+ if (rule && !rule._meta.isTrivia) {
9592
+ ;
9593
+ rule._meta.grammarScanSkip = gScanSkip;
9594
+ }
9595
+ }
9596
+ }
9199
9597
  return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
9200
9598
  };
9201
9599
  const compileRulesFactory = (init, label2) => {
9202
9600
  const evaluated = evaluateRulesFactory(init, label2);
9203
9601
  if (!evaluated) return null;
9204
9602
  const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
9205
- if (!compiled) {
9206
- warn(init.start, `${label2}: rule map couldn't be inlined`);
9207
- return null;
9208
- }
9209
- return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
9603
+ return { replacement: compiled?.replacement ?? null, ruleMap: evaluated.ruleMap, ...compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
9210
9604
  };
9211
9605
  const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
9212
9606
  const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
@@ -9301,6 +9695,17 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9301
9695
  }
9302
9696
  return out;
9303
9697
  };
9698
+ const carriedRuleMaps = (items) => {
9699
+ const out = [];
9700
+ const add = (it) => {
9701
+ if (isIR(it)) out.push(evalRuleMapIR(it.ir));
9702
+ };
9703
+ for (const it of items) {
9704
+ if (isSpread(it)) for (const p of importedPieces(it.__spreadLocal) ?? []) add(p);
9705
+ else add(it);
9706
+ }
9707
+ return out;
9708
+ };
9304
9709
  const materializeLeafCoverage = (items, localRules, localNs, composing, captureTerminals = false) => {
9305
9710
  const sources = [];
9306
9711
  const add = (item) => {
@@ -9332,12 +9737,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9332
9737
  }
9333
9738
  return pieces;
9334
9739
  };
9335
- const localCarried = (rm, label2, composing) => {
9740
+ const localCarried = (rm, label2, composing, scanSkip) => {
9336
9741
  const entries = [...rm];
9337
9742
  const ns = nsFor(label2);
9338
- const ir = serializeRuleMap(entries);
9743
+ const ir = serializeRuleMap(entries, scanSkip);
9339
9744
  if (ir) return { carried: [{ ns, ir }] };
9340
- const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, recovery });
9745
+ const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
9341
9746
  return p ? { carried: [p] } : null;
9342
9747
  };
9343
9748
  const argPieces = (arg, label2, composing) => {
@@ -9362,18 +9767,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9362
9767
  }
9363
9768
  }
9364
9769
  if (isRulesCall(arg)) {
9365
- const rulesArgs = arg.arguments;
9366
- const a0 = rulesArgs[0];
9367
- const a1 = rulesArgs[1];
9368
- const optionsFirst = a0?.type === "ObjectExpression";
9369
- const factory = optionsFirst ? a1 : a0;
9370
- const rm = factory ? evaluateParserFactory(factory, scope, code, []) : null;
9371
- return rm ? localCarried(rm, label2, composing) : null;
9770
+ const evaluated = evaluateRulesFactory(arg, label2);
9771
+ return evaluated ? localCarried(evaluated.ruleMap, label2, composing, evaluated.scanSkip) : null;
9372
9772
  }
9373
9773
  if (arg.type === "Identifier") {
9374
9774
  const name = arg.name;
9375
9775
  const rm = localRuleMaps.get(name);
9376
- if (rm) return localCarried(rm, label2, composing);
9776
+ if (rm) return localCarried(rm, label2, composing, localGrammarScanSkip.get(name));
9377
9777
  const composed = localComposedCarried.get(name);
9378
9778
  if (composed) return { carried: composed };
9379
9779
  const pieces = importedPieces(name);
@@ -9439,6 +9839,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9439
9839
  }
9440
9840
  carried.push(...r.carried);
9441
9841
  }
9842
+ runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
9442
9843
  const pieces = materializeCarried(carried, composing);
9443
9844
  try {
9444
9845
  return { replacement: emitFusedSource(pieces), carried, ...composing ? { trivia: composing } : {} };
@@ -9487,6 +9888,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9487
9888
  }
9488
9889
  try {
9489
9890
  const localNs = nsFor(`composeLeaf${init.start}`);
9891
+ runFusedGatingDiagnostic(() => [...carriedRuleMaps(carried), [...localRules]]);
9490
9892
  const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
9491
9893
  if (!plainLocalPiece) {
9492
9894
  warn(init.start, "composeLeaf(): local rules could not be statically compiled");
@@ -9575,7 +9977,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9575
9977
  const refEntry = scope.get(varName);
9576
9978
  const refCombi = refEntry?.combi ?? null;
9577
9979
  if (refCombi) {
9578
- const compiled2 = compile(refCombi, void 0, { recovery, coverage: grammarCoverage });
9980
+ const compiled2 = compile(refCombi, void 0, { recovery, coverage: grammarCoverage, gating: { entryName: varName } });
9579
9981
  if (compiled2.inlineExpression === null) {
9580
9982
  warn(init.start, `"${varName}" is a ref() that couldn't be inlined (was .define() called with a static combinator?)`);
9581
9983
  continue;
@@ -9590,17 +9992,26 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9590
9992
  localRuleMaps.set(varName, compiledRules.ruleMap);
9591
9993
  if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia);
9592
9994
  if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip);
9995
+ const ns = nsFor(varName);
9996
+ const pieces = exportPrefix || compiledRules.replacement === null ? compileLinkable([...compiledRules.ruleMap], ns, {
9997
+ ...compiledRules.scanSkip ? { scanSkip: compiledRules.scanSkip } : {},
9998
+ recovery
9999
+ }) : null;
10000
+ if (compiledRules.replacement === null) {
10001
+ if (!pieces || !hasExternalRuleRef([...compiledRules.ruleMap])) {
10002
+ warn(init.start, `${varName}: rule map couldn't be inlined`);
10003
+ continue;
10004
+ }
10005
+ keepMacroImport = true;
10006
+ }
10007
+ const source = compiledRules.replacement ?? code.slice(init.start, init.end);
9593
10008
  let replacement = withCoverageDefinitions(
9594
- compiledRules.replacement,
9595
- compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(compiledRules.replacement)
10009
+ source,
10010
+ compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(source)
9596
10011
  );
9597
- if (exportPrefix) {
9598
- const ns = nsFor(varName);
9599
- const pieces = compileLinkable([...compiledRules.ruleMap], ns, { recovery });
9600
- if (pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
9601
- const ir = serializeRuleMap([...compiledRules.ruleMap]);
9602
- replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
9603
- }
10012
+ if (exportPrefix && pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
10013
+ const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
10014
+ replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
9604
10015
  }
9605
10016
  replacements.push({ start: init.start, end: init.end, replacement });
9606
10017
  continue;
@@ -9643,7 +10054,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9643
10054
  warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`);
9644
10055
  continue;
9645
10056
  }
9646
- const compiled = compile(parser2, void 0, { recovery, coverage: grammarCoverage });
10057
+ const compiled = compile(parser2, void 0, { recovery, coverage: grammarCoverage, gating: { entryName: varName } });
9647
10058
  if (compiled.inlineExpression === null) {
9648
10059
  warn(init.start, `"${varName}" couldn't be inlined (likely closes over a runtime value)`);
9649
10060
  continue;
@@ -9662,6 +10073,10 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9662
10073
  }
9663
10074
  const compiledRules = compileRulesFactory(init, "{ \u2026 }");
9664
10075
  if (!compiledRules) continue;
10076
+ if (compiledRules.replacement === null) {
10077
+ warn(init.start, `{ \u2026 }: rule map couldn't be inlined`);
10078
+ continue;
10079
+ }
9665
10080
  const pattern = d.id;
9666
10081
  const bindings = [];
9667
10082
  let allOk = true;
@@ -9703,7 +10118,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9703
10118
  }
9704
10119
  if (replacements.length === 0 && warnings.length === 0) return null;
9705
10120
  for (const imp of macroImports) {
9706
- imp.fullyResolved = !anyUnresolved;
10121
+ imp.fullyResolved = !anyUnresolved && !keepMacroImport;
9707
10122
  }
9708
10123
  const ms = new import_magic_string.default(code);
9709
10124
  if (!anyUnresolved) {