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
@@ -7,6 +7,11 @@ import { ResolverFactory } from "oxc-resolver";
7
7
  import MagicString from "magic-string";
8
8
 
9
9
  // src/combinators/first-set.ts
10
+ function resolveNamedRef(p, resolve2) {
11
+ if (resolve2 === void 0) return void 0;
12
+ const name = p._ruleName;
13
+ return name === void 0 ? void 0 : resolve2(name);
14
+ }
10
15
  function union(a, b) {
11
16
  if (a.kind === "any" || b.kind === "any") return { kind: "any" };
12
17
  if (a.kind === "empty") return b;
@@ -44,10 +49,10 @@ function any() {
44
49
  function empty() {
45
50
  return { kind: "empty" };
46
51
  }
47
- function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
52
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve2) {
48
53
  if (seen.has(p)) return true;
49
54
  seen.add(p);
50
- const me = (c) => matchesEmpty(c, seen);
55
+ const me = (c) => matchesEmpty(c, seen, resolve2);
51
56
  const d = p._def;
52
57
  switch (d.tag) {
53
58
  case "literal":
@@ -64,8 +69,16 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
64
69
  case "many":
65
70
  case "optional":
66
71
  case "not":
72
+ case "peek":
67
73
  return true;
68
74
  // zero repetitions / absent / lookahead
75
+ // Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
76
+ // Any `min >= 1` requires that many ITEMS, so it is nullable only when the
77
+ // item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
78
+ // nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
79
+ // the gating diagnostic for a list that can never match empty.)
80
+ case "sepBy":
81
+ return d.min >= 1 ? me(d.parser) : true;
69
82
  case "oneOrMore":
70
83
  return me(d.parser);
71
84
  case "sequence":
@@ -89,7 +102,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
89
102
  try {
90
103
  return me(d.thunk());
91
104
  } catch {
92
- return true;
105
+ const t = resolveNamedRef(p, resolve2);
106
+ return t ? me(t) : true;
93
107
  }
94
108
  default:
95
109
  return true;
@@ -98,18 +112,45 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
98
112
  function isZeroWidthAssertion(p) {
99
113
  return p._def.tag === "not";
100
114
  }
115
+ function isPositiveLookahead(p) {
116
+ return p._def.tag === "peek";
117
+ }
118
+ function narrowBy(acc, constraint) {
119
+ if (constraint.kind === "any") return acc;
120
+ if (acc === null) return constraint;
121
+ if (acc.kind === "any") return constraint;
122
+ if (acc.kind === "empty" || constraint.kind === "empty") return { kind: "empty" };
123
+ const ranges = [];
124
+ for (const a of acc.ranges) for (const b of constraint.ranges) {
125
+ const lo = Math.max(a.lo, b.lo);
126
+ const hi = Math.min(a.hi, b.hi);
127
+ if (lo <= hi) ranges.push({ lo, hi });
128
+ }
129
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
130
+ }
131
+ function applyAssertion(fs2, assertion) {
132
+ if (assertion === null) return fs2;
133
+ if (fs2.kind === "empty") return assertion;
134
+ return narrowBy(fs2, assertion) ?? fs2;
135
+ }
101
136
  function sequenceFirstSet(parsers) {
102
137
  let fs2 = empty();
138
+ let assertion = null;
103
139
  for (const p of parsers) {
140
+ if (isPositiveLookahead(p)) {
141
+ assertion = narrowBy(assertion, p._meta.firstSet);
142
+ continue;
143
+ }
104
144
  if (!isZeroWidthAssertion(p)) fs2 = union(fs2, p._meta.firstSet);
105
- if (!matchesEmpty(p)) return fs2;
145
+ if (!matchesEmpty(p)) return applyAssertion(fs2, assertion);
106
146
  }
107
- return fs2;
147
+ return applyAssertion(fs2, assertion);
108
148
  }
109
- function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
149
+ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve2) {
110
150
  if (seen.has(p)) return any();
111
151
  seen.add(p);
112
- const fs2 = (c) => firstSetOf(c, seen);
152
+ const fs2 = (c) => firstSetOf(c, seen, resolve2);
153
+ const empties = (c) => matchesEmpty(c, /* @__PURE__ */ new Set(), resolve2);
113
154
  const d = p._def;
114
155
  switch (d.tag) {
115
156
  case "literal":
@@ -121,7 +162,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
121
162
  try {
122
163
  return fs2(d.thunk());
123
164
  } catch {
124
- return any();
165
+ const t = resolveNamedRef(p, resolve2);
166
+ return t ? fs2(t) : any();
125
167
  }
126
168
  case "choice": {
127
169
  let out = empty();
@@ -130,11 +172,20 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
130
172
  }
131
173
  case "sequence": {
132
174
  let out = empty();
175
+ let assertion = null;
133
176
  for (const term of d.parsers) {
177
+ if (isPositiveLookahead(term)) {
178
+ assertion = narrowBy(assertion, fs2(term));
179
+ continue;
180
+ }
134
181
  if (!isZeroWidthAssertion(term)) out = union(out, fs2(term));
135
- if (!matchesEmpty(term)) return out;
182
+ if (!empties(term)) return applyAssertion(out, assertion);
136
183
  }
137
- return out;
184
+ return applyAssertion(out, assertion);
185
+ }
186
+ case "peek": {
187
+ const inner = d.parser;
188
+ return empties(inner) ? any() : fs2(inner);
138
189
  }
139
190
  case "oneOrMore":
140
191
  case "many":
@@ -150,6 +201,7 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set()) {
150
201
  return fs2(d.parser);
151
202
  case "sepBy":
152
203
  return fs2(d.parser);
204
+ // both min 0 and min 1 start with the item
153
205
  case "skip":
154
206
  return fs2(d.main);
155
207
  default:
@@ -871,9 +923,46 @@ function firstSetFromRegex(source) {
871
923
  return { firstSet: firstSet(ast), canMatchNewline: firstCanBeNewline(ast) };
872
924
  }
873
925
 
926
+ // src/combinators/case-fold.ts
927
+ function canonical(ch2) {
928
+ const u = ch2.toUpperCase();
929
+ if (u.length !== 1) return ch2;
930
+ if (ch2.codePointAt(0) >= 128 && u.codePointAt(0) < 128) return ch2;
931
+ return u;
932
+ }
933
+ var foldIndex = null;
934
+ function buildFoldIndex() {
935
+ const byCanonical = /* @__PURE__ */ new Map();
936
+ for (let cp = 0; cp < 65536; cp++) {
937
+ if (cp >= 55296 && cp <= 57343) continue;
938
+ const key = canonical(String.fromCharCode(cp));
939
+ const bucket = byCanonical.get(key);
940
+ if (bucket) bucket.push(cp);
941
+ else byCanonical.set(key, [cp]);
942
+ }
943
+ const index = /* @__PURE__ */ new Map();
944
+ for (const members of byCanonical.values()) {
945
+ if (members.length < 2) continue;
946
+ for (const cp of members) index.set(cp, members);
947
+ }
948
+ return index;
949
+ }
950
+ function caseFoldVariants(cp) {
951
+ if (cp < 128) {
952
+ if (cp >= 65 && cp <= 90) return [cp, cp + 32];
953
+ if (cp >= 97 && cp <= 122) return [cp, cp - 32];
954
+ return [cp];
955
+ }
956
+ if (cp > 65535) return [cp];
957
+ foldIndex ??= buildFoldIndex();
958
+ return foldIndex.get(cp) ?? [cp];
959
+ }
960
+
874
961
  // src/combinators/keywords.ts
875
- function word(str, boundary = "_0-9A-Za-z") {
876
- return keywords([str], { boundary });
962
+ function word(str, boundaryOrOpts, opts) {
963
+ const boundary = typeof boundaryOrOpts === "string" ? boundaryOrOpts : "_0-9A-Za-z";
964
+ const rest = typeof boundaryOrOpts === "object" && boundaryOrOpts !== null ? boundaryOrOpts : opts;
965
+ return keywords([str], { ...rest, boundary });
877
966
  }
878
967
  function escapeRe(s) {
879
968
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -882,7 +971,7 @@ function keywords(words, opts = {}) {
882
971
  const sorted = [...new Set(words)].sort((a, b) => b.length - a.length);
883
972
  const alt = sorted.map(escapeRe).join("|");
884
973
  const boundary = opts.boundary ? `(?![${opts.boundary}])` : "";
885
- const flags = opts.caseInsensitive ? "iuy" : "uy";
974
+ const flags = opts.caseInsensitive ? "iy" : "uy";
886
975
  const re = new RegExp(`(?:${alt})${boundary}`, flags);
887
976
  let firstSet2 = empty();
888
977
  for (const w of sorted) {
@@ -890,10 +979,7 @@ function keywords(words, opts = {}) {
890
979
  const cp = w.codePointAt(0);
891
980
  firstSet2 = union(firstSet2, fromChar(cp));
892
981
  if (opts.caseInsensitive) {
893
- const u = String.fromCodePoint(cp).toUpperCase().codePointAt(0);
894
- const l = String.fromCodePoint(cp).toLowerCase().codePointAt(0);
895
- if (u !== void 0) firstSet2 = union(firstSet2, fromChar(u));
896
- if (l !== void 0) firstSet2 = union(firstSet2, fromChar(l));
982
+ for (const v2 of caseFoldVariants(cp)) firstSet2 = union(firstSet2, fromChar(v2));
897
983
  }
898
984
  }
899
985
  const meta = { firstSet: firstSet2, canMatchNewline: false, isTrivia: false };
@@ -1041,6 +1127,7 @@ function deriveExpected(c) {
1041
1127
  case "oneOrMore":
1042
1128
  case "transform":
1043
1129
  case "not":
1130
+ case "peek":
1044
1131
  return deriveExpected(def.parser);
1045
1132
  case "lazy": {
1046
1133
  const name = c._ruleName;
@@ -1511,6 +1598,13 @@ function rollbackTrivia(ctx, mark) {
1511
1598
  rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors });
1512
1599
  if (ctx._triviaLog) ctx._triviaLog.length = mark.log;
1513
1600
  }
1601
+ function saveLookaheadMark(ctx) {
1602
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null };
1603
+ }
1604
+ function rollbackLookahead(ctx, mark) {
1605
+ rollbackTrivia(ctx, mark);
1606
+ if (ctx._probe) ctx._probe.best = mark.probeBest;
1607
+ }
1514
1608
  function scanWithLabels(input, cur, ctx) {
1515
1609
  const triviaP = ctx.trivia;
1516
1610
  const spec = analyzeLabeledTrivia(triviaP);
@@ -1908,13 +2002,26 @@ function repItem(combinator, input, cur, ctx, guardable) {
1908
2002
  }
1909
2003
  return { value: result.value, end: result.span.end };
1910
2004
  }
1911
- function many(combinator) {
2005
+ function resolveBounds(what, opts) {
2006
+ const min = opts.min ?? 0;
2007
+ const max = opts.max ?? Infinity;
2008
+ const bad = (msg) => {
2009
+ throw new RangeError(`parseman: ${what} ${msg}`);
2010
+ };
2011
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`);
2012
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`);
2013
+ if (max < min) bad(`max (${max}) is less than min (${min}) \u2014 the combinator could never succeed`);
2014
+ return { min, max };
2015
+ }
2016
+ function many(combinator, opts = {}) {
2017
+ const { min, max } = resolveBounds("many()", opts);
2018
+ if (min >= 1) return atLeast(combinator, min, max);
1912
2019
  const meta = {
1913
2020
  firstSet: combinator._meta.firstSet,
1914
2021
  canMatchNewline: combinator._meta.canMatchNewline,
1915
2022
  isTrivia: false
1916
2023
  };
1917
- const def = { tag: "many", parser: combinator, min: 0 };
2024
+ const def = { tag: "many", parser: combinator, min: 0, ...max === Infinity ? {} : { max } };
1918
2025
  let expected;
1919
2026
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1920
2027
  return {
@@ -1924,7 +2031,9 @@ function many(combinator) {
1924
2031
  parse(input, pos, ctx) {
1925
2032
  const values = def.valueUnused ? void 0 : [];
1926
2033
  let cur = pos;
2034
+ let count = 0;
1927
2035
  while (cur < input.length) {
2036
+ if (count >= max) break;
1928
2037
  const item = repItem(combinator, input, cur, ctx, guardable);
1929
2038
  if (item === "stop") break;
1930
2039
  if ("fail" in item) {
@@ -1935,23 +2044,28 @@ function many(combinator) {
1935
2044
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1936
2045
  if (values !== void 0) values.push(error);
1937
2046
  captureError(ctx, error);
2047
+ count++;
1938
2048
  cur = end;
1939
2049
  continue;
1940
2050
  }
1941
2051
  if (values !== void 0) values.push(item.value);
2052
+ count++;
1942
2053
  cur = item.end;
1943
2054
  }
1944
2055
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1945
2056
  }
1946
2057
  };
1947
2058
  }
1948
- function oneOrMore(combinator) {
2059
+ function oneOrMore(combinator, opts = {}) {
2060
+ return many(combinator, { ...opts, min: opts.min ?? 1 });
2061
+ }
2062
+ function atLeast(combinator, min, max) {
1949
2063
  const meta = {
1950
2064
  firstSet: combinator._meta.firstSet,
1951
2065
  canMatchNewline: combinator._meta.canMatchNewline,
1952
2066
  isTrivia: false
1953
2067
  };
1954
- const def = { tag: "oneOrMore", parser: combinator, min: 1 };
2068
+ const def = { tag: "oneOrMore", parser: combinator, min, ...max === Infinity ? {} : { max } };
1955
2069
  let expected;
1956
2070
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
1957
2071
  return {
@@ -1963,7 +2077,19 @@ function oneOrMore(combinator) {
1963
2077
  if (!first.ok) return first;
1964
2078
  const values = def.valueUnused ? void 0 : [first.value];
1965
2079
  let cur = first.span.end;
2080
+ let count = 1;
2081
+ while (count < min) {
2082
+ const item = repItem(combinator, input, cur, ctx, guardable);
2083
+ if (item === "stop" || "fail" in item) {
2084
+ expected ??= deriveExpected(combinator);
2085
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
2086
+ }
2087
+ if (values !== void 0) values.push(item.value);
2088
+ cur = item.end;
2089
+ count++;
2090
+ }
1966
2091
  while (cur < input.length) {
2092
+ if (count >= max) break;
1967
2093
  const item = repItem(combinator, input, cur, ctx, guardable);
1968
2094
  if (item === "stop") break;
1969
2095
  if ("fail" in item) {
@@ -1974,10 +2100,12 @@ function oneOrMore(combinator) {
1974
2100
  const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected);
1975
2101
  if (values !== void 0) values.push(error);
1976
2102
  captureError(ctx, error);
2103
+ count++;
1977
2104
  cur = end;
1978
2105
  continue;
1979
2106
  }
1980
2107
  if (values !== void 0) values.push(item.value);
2108
+ count++;
1981
2109
  cur = item.end;
1982
2110
  }
1983
2111
  return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
@@ -2007,17 +2135,33 @@ function optional(combinator) {
2007
2135
  }
2008
2136
  };
2009
2137
  }
2010
- function sepBy(combinator, separator) {
2138
+ function oneOrMoreSep(combinator, separator, opts = {}) {
2139
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 });
2140
+ }
2141
+ function sepBy(combinator, separator, opts = {}) {
2142
+ const { min, max } = resolveBounds("sepBy()", opts);
2143
+ const trailing = opts.trailing ?? "forbid";
2011
2144
  const meta = {
2012
2145
  firstSet: combinator._meta.firstSet,
2013
2146
  canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
2014
2147
  isTrivia: false
2015
2148
  };
2016
2149
  let expected;
2150
+ const failAt2 = (at) => {
2151
+ expected ??= deriveExpected(combinator);
2152
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } };
2153
+ };
2017
2154
  return {
2018
2155
  _tag: "sepBy",
2019
2156
  _meta: meta,
2020
- _def: { tag: "sepBy", parser: combinator, separator },
2157
+ _def: {
2158
+ tag: "sepBy",
2159
+ parser: combinator,
2160
+ separator,
2161
+ min,
2162
+ ...max === Infinity ? {} : { max },
2163
+ ...trailing === "forbid" ? {} : { trailing }
2164
+ },
2021
2165
  parse(input, pos, ctx) {
2022
2166
  const first = combinator.parse(input, pos, ctx);
2023
2167
  const values = [];
@@ -2028,6 +2172,7 @@ function sepBy(combinator, separator) {
2028
2172
  } else {
2029
2173
  const term = ctx._tolerant ? ctx._sync : void 0;
2030
2174
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2175
+ if (min >= 1) return failAt2(pos);
2031
2176
  return { ok: true, value: [], span: { start: pos, end: pos } };
2032
2177
  }
2033
2178
  expected ??= deriveExpected(combinator);
@@ -2037,6 +2182,7 @@ function sepBy(combinator, separator) {
2037
2182
  cur = rec.end;
2038
2183
  }
2039
2184
  while (cur < input.length) {
2185
+ if (values.length >= max) break;
2040
2186
  const loopMark = saveTriviaMark(ctx);
2041
2187
  let sepPos = cur;
2042
2188
  if (ctx.trivia) {
@@ -2053,6 +2199,7 @@ function sepBy(combinator, separator) {
2053
2199
  rollbackTrivia(ctx, loopMark);
2054
2200
  break;
2055
2201
  }
2202
+ const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
2056
2203
  let nextPos = sep2.span.end;
2057
2204
  if (ctx.trivia) {
2058
2205
  if (needsDeferredTriviaCommit(ctx)) {
@@ -2074,12 +2221,18 @@ function sepBy(combinator, separator) {
2074
2221
  cur = rec.end;
2075
2222
  continue;
2076
2223
  }
2224
+ if (sepMark !== void 0) {
2225
+ rollbackTrivia(ctx, sepMark);
2226
+ cur = sep2.span.end;
2227
+ break;
2228
+ }
2077
2229
  rollbackTrivia(ctx, loopMark);
2078
2230
  break;
2079
2231
  }
2080
2232
  values.push(next.value);
2081
2233
  cur = next.span.end;
2082
2234
  }
2235
+ if (values.length < min) return failAt2(cur);
2083
2236
  return { ok: true, value: values, span: { start: pos, end: cur } };
2084
2237
  }
2085
2238
  };
@@ -2132,6 +2285,7 @@ function markUnusedValues(root) {
2132
2285
  visit(def.separator, false);
2133
2286
  return;
2134
2287
  case "not":
2288
+ case "peek":
2135
2289
  visit(def.parser, false);
2136
2290
  return;
2137
2291
  case "grammar":
@@ -2250,9 +2404,9 @@ function not(combinator) {
2250
2404
  _meta: meta,
2251
2405
  _def: { tag: "not", parser: combinator },
2252
2406
  parse(input, pos, ctx) {
2253
- const mark = saveCstMark(ctx);
2407
+ const mark = saveLookaheadMark(ctx);
2254
2408
  const result = combinator.parse(input, pos, ctx);
2255
- rollbackCstCapture(ctx, mark);
2409
+ rollbackLookahead(ctx, mark);
2256
2410
  if (result.ok) {
2257
2411
  return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } };
2258
2412
  }
@@ -2261,6 +2415,28 @@ function not(combinator) {
2261
2415
  };
2262
2416
  }
2263
2417
 
2418
+ // src/combinators/peek.ts
2419
+ function peek(combinator) {
2420
+ const meta = {
2421
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
2422
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
2423
+ canMatchNewline: false,
2424
+ isTrivia: false
2425
+ };
2426
+ return {
2427
+ _tag: "peek",
2428
+ _meta: meta,
2429
+ _def: { tag: "peek", parser: combinator },
2430
+ parse(input, pos, ctx) {
2431
+ const mark = saveLookaheadMark(ctx);
2432
+ const result = combinator.parse(input, pos, ctx);
2433
+ rollbackLookahead(ctx, mark);
2434
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } };
2435
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } };
2436
+ }
2437
+ };
2438
+ }
2439
+
2264
2440
  // src/compiler/build-arity.ts
2265
2441
  var PARAM_LIST_RE = (
2266
2442
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
@@ -2359,6 +2535,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2359
2535
  case "expect":
2360
2536
  case "withCtx":
2361
2537
  case "not":
2538
+ case "peek":
2362
2539
  return parserHasOwnFields(d.parser, seen);
2363
2540
  default:
2364
2541
  return false;
@@ -2398,6 +2575,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2398
2575
  case "expect":
2399
2576
  case "withCtx":
2400
2577
  case "not":
2578
+ case "peek":
2401
2579
  case "field":
2402
2580
  return parserHasTriviaSite(d.parser, seen);
2403
2581
  case "choice":
@@ -2447,6 +2625,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2447
2625
  case "expect":
2448
2626
  case "withCtx":
2449
2627
  case "not":
2628
+ case "peek":
2450
2629
  return parserEnablesTriviaCapture(d.parser, seen);
2451
2630
  // leaf() clears the inner CST/trivia collector and publishes only its own
2452
2631
  // terminal result. An inner parser() capture cannot reach this node.
@@ -2795,7 +2974,7 @@ function leaf(root, fn) {
2795
2974
  }
2796
2975
 
2797
2976
  // src/version.ts
2798
- var PARSEMAN_VERSION = "0.32.0";
2977
+ var PARSEMAN_VERSION = "0.34.0";
2799
2978
 
2800
2979
  // src/analysis/gating.ts
2801
2980
  var isAny = (fs2) => fs2.kind === "any";
@@ -2835,14 +3014,15 @@ function firstSetToString(fs2) {
2835
3014
  var ruleNameOf = (p) => p._ruleName;
2836
3015
  var SUGGESTIONS = {
2837
3016
  "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.",
2838
- "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.",
2839
- "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.",
3017
+ "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.",
3018
+ "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.",
2840
3019
  "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.",
2841
3020
  "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
2842
3021
  "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
2843
3022
  };
2844
- function classifyBroadArm(arm) {
3023
+ function classifyBroadArm(arm, resolve2) {
2845
3024
  const seen = /* @__PURE__ */ new Set();
3025
+ const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
2846
3026
  const walk2 = (p) => {
2847
3027
  if (seen.has(p)) return { cause: "ref-cycle", detail: "ref cycle" };
2848
3028
  seen.add(p);
@@ -2854,6 +3034,10 @@ function classifyBroadArm(arm) {
2854
3034
  return isAny(p._meta.firstSet) ? { cause: "broad-recognizer", detail: `broad recognizer (${d.tag})` } : null;
2855
3035
  case "not":
2856
3036
  return { cause: "leading-not", detail: "leading not(...) (first-set ANY)" };
3037
+ // peek() CARRIES its body's first-set, so it is only broad when the body is
3038
+ // (or the body is nullable) — walk in for the real cause.
3039
+ case "peek":
3040
+ return walk2(d.parser);
2857
3041
  case "scanTo":
2858
3042
  return { cause: "broad-recognizer", detail: "scanTo (any first char)" };
2859
3043
  // `guard` (the `gate()` state predicate) has a genuinely-`any` first-set of
@@ -2870,18 +3054,23 @@ function classifyBroadArm(arm) {
2870
3054
  return walk2(d.parser);
2871
3055
  case "lazy": {
2872
3056
  const name = p._ruleName;
3057
+ let target;
2873
3058
  try {
2874
- const inner = walk2(d.thunk());
2875
- if (name !== void 0) return inner ? { cause: "cross-artifact-ref", detail: `via ref g.${name} \u2192 ${inner.detail}` } : null;
2876
- return inner;
3059
+ target = d.thunk();
2877
3060
  } catch {
2878
- return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}` };
3061
+ const bound = name !== void 0 ? resolve2?.(name) : void 0;
3062
+ if (bound === void 0)
3063
+ return { cause: "cross-artifact-ref", detail: `unresolved ref${name ? ` g.${name}` : ""}`, unresolvedExternal: name !== void 0 };
3064
+ target = bound;
2879
3065
  }
3066
+ const inner = walk2(target);
3067
+ if (name !== void 0) return inner ? relabel(inner, "cross-artifact-ref", `via ref g.${name} \u2192 ${inner.detail}`) : null;
3068
+ return inner;
2880
3069
  }
2881
3070
  case "optional":
2882
3071
  case "many": {
2883
3072
  const inner = walk2(d.parser);
2884
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3073
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2885
3074
  }
2886
3075
  case "oneOrMore":
2887
3076
  case "transform":
@@ -2903,13 +3092,13 @@ function classifyBroadArm(arm) {
2903
3092
  sawNullablePrefix = true;
2904
3093
  continue;
2905
3094
  }
2906
- if (isAny(firstSetOf(t))) {
3095
+ if (isAny(firstSetOf(t, /* @__PURE__ */ new Set(), resolve2))) {
2907
3096
  const inner = walk2(t);
2908
3097
  if (sawNullablePrefix)
2909
- return { cause: "nullable-prefix", detail: inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term" };
3098
+ return relabel(inner, "nullable-prefix", inner ? `nullable prefix \u2192 ${inner.detail}` : "nullable prefix to broad term");
2910
3099
  return inner ?? { cause: "broad-recognizer", detail: "sequence leading term is broad" };
2911
3100
  }
2912
- if (matchesEmpty(t)) {
3101
+ if (matchesEmpty(t, /* @__PURE__ */ new Set(), resolve2)) {
2913
3102
  sawNullablePrefix = true;
2914
3103
  continue;
2915
3104
  }
@@ -2920,7 +3109,7 @@ function classifyBroadArm(arm) {
2920
3109
  case "choice": {
2921
3110
  for (const a of d.parsers) {
2922
3111
  const r = walk2(a);
2923
- if (r) return { cause: r.cause, detail: `choice arm \u2192 ${r.detail}` };
3112
+ if (r) return relabel(r, r.cause, `choice arm \u2192 ${r.detail}`);
2924
3113
  }
2925
3114
  return null;
2926
3115
  }
@@ -2957,7 +3146,7 @@ function detectAntiPatterns(rule, arms) {
2957
3146
  kind: "double-not",
2958
3147
  rule,
2959
3148
  armIndex: i,
2960
- 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."
3149
+ 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."
2961
3150
  });
2962
3151
  } else {
2963
3152
  out.push({
@@ -2980,6 +3169,9 @@ function detectAntiPatterns(rule, arms) {
2980
3169
  return out;
2981
3170
  }
2982
3171
  function analyzeGating(entry, opts) {
3172
+ return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3173
+ }
3174
+ function analyzeGatingRules(ruleMap, opts) {
2983
3175
  const raw = [];
2984
3176
  const antiPatterns = [];
2985
3177
  const visited = /* @__PURE__ */ new Set();
@@ -2989,7 +3181,7 @@ function analyzeGating(entry, opts) {
2989
3181
  const d = p._def;
2990
3182
  const rule = ruleNameOf(p) ?? enclosingRule;
2991
3183
  if (d.tag === "choice") {
2992
- raw.push({ g: analyzeChoice(p, d, rule), rule });
3184
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
2993
3185
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
2994
3186
  }
2995
3187
  const rec = d;
@@ -3006,7 +3198,7 @@ function analyzeGating(entry, opts) {
3006
3198
  }
3007
3199
  for (const k of kids) visit(k, rule);
3008
3200
  };
3009
- visit(entry, ruleNameOf(entry) ?? "<entry>");
3201
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3010
3202
  const perRule = /* @__PURE__ */ new Map();
3011
3203
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3012
3204
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3022,18 +3214,19 @@ function analyzeGating(entry, opts) {
3022
3214
  });
3023
3215
  const gated = choices.filter((c) => c.gates === "yes").length;
3024
3216
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
3025
- const ungated = choices.filter((c) => c.gates === "no" && !c.accepted);
3217
+ const ungated = choices.filter((c) => c.gates === "no" && !c.accepted && !c.deferred);
3026
3218
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3219
+ const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3027
3220
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3028
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, acceptedUnused, choices, antiPatterns };
3221
+ return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3029
3222
  }
3030
- function analyzeChoice(p, d, rule) {
3223
+ function analyzeChoice(p, d, rule, resolve2) {
3031
3224
  const arms = d.parsers;
3032
3225
  const shallow = arms.map((a) => a._meta.firstSet);
3033
- const deep = arms.map((a) => firstSetOf(a));
3226
+ const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
3034
3227
  const anyArms = deep.map((fs2, index) => ({ fs: fs2, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
3035
- const { cause, detail } = classifyBroadArm(arms[index]);
3036
- return { index, cause, detail, shallowAnyOnly: false, suggestion: SUGGESTIONS[cause] };
3228
+ const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve2);
3229
+ return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
3037
3230
  });
3038
3231
  const overlaps = [];
3039
3232
  for (let i = 0; i < deep.length; i++)
@@ -3049,10 +3242,12 @@ function analyzeChoice(p, d, rule) {
3049
3242
  if (d.disjoint) gates = "yes";
3050
3243
  else if (anyArms.length === 0 && overlaps.length === 0) gates = "recoverable";
3051
3244
  else gates = "no";
3245
+ const deferred = gates === "no" && overlaps.length === 0 && anyArms.length > 0 && anyArms.every((a) => a.unresolvedExternal);
3052
3246
  return {
3053
3247
  rule,
3054
3248
  strategy: d.strategy?.tag ?? "firstMatch",
3055
3249
  gates,
3250
+ deferred,
3056
3251
  combinedFirstSet: { shallow: combine(shallow), deep: combine(deep) },
3057
3252
  anyArms,
3058
3253
  overlaps
@@ -3219,6 +3414,7 @@ function children(def, winners) {
3219
3414
  case "field":
3220
3415
  case "grammar":
3221
3416
  case "not":
3417
+ case "peek":
3222
3418
  case "node":
3223
3419
  case "guard":
3224
3420
  case "withCtx":
@@ -4992,6 +5188,9 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
4992
5188
  case "keywords":
4993
5189
  case "guard":
4994
5190
  case "not":
5191
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
5192
+ // outcomes, so it can never leave a partial capture behind.
5193
+ case "peek":
4995
5194
  case "trivia":
4996
5195
  case "token":
4997
5196
  case "leaf":
@@ -5063,6 +5262,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
5063
5262
  case "leaf":
5064
5263
  return true;
5065
5264
  case "not":
5265
+ case "peek":
5066
5266
  case "guard":
5067
5267
  case "trivia":
5068
5268
  case "unknown":
@@ -5189,6 +5389,7 @@ function emitKeywordsFast(def, ctx, pos) {
5189
5389
  if (def.words.length === 0 || def.words.some((w) => w.length === 0)) return null;
5190
5390
  if (def.words.some((w) => Array.from(w).length !== w.length)) return null;
5191
5391
  if (def.caseInsensitive && def.boundary) return null;
5392
+ if (def.caseInsensitive && def.words.some((w) => w.split("").some((c) => c.charCodeAt(0) > 127))) return null;
5192
5393
  let boundary = null;
5193
5394
  if (def.boundary) {
5194
5395
  let body = def.boundary;
@@ -5236,7 +5437,7 @@ function emitKeywords(def, ctx, pos) {
5236
5437
  if (fast) return fast;
5237
5438
  const alt = def.words.map(escapeKeywordRe).join("|");
5238
5439
  const boundary = def.boundary ? `(?![${def.boundary}])` : "";
5239
- const flags = def.caseInsensitive ? "iuy" : "uy";
5440
+ const flags = def.caseInsensitive ? "iy" : "uy";
5240
5441
  const source = `(?:${alt})${boundary}`;
5241
5442
  const key = `${source}/${flags}`;
5242
5443
  let rName = ctx.regexMap.get(key);
@@ -5382,6 +5583,7 @@ function failsAtStart(p) {
5382
5583
  case "keywords":
5383
5584
  case "guard":
5384
5585
  case "not":
5586
+ case "peek":
5385
5587
  return true;
5386
5588
  case "transform":
5387
5589
  case "label":
@@ -5795,14 +5997,17 @@ function emitMany(def, ctx, pos) {
5795
5997
  ];
5796
5998
  const mySyncV = ctx.recovery ? v(ctx, "_mysy") : "";
5797
5999
  if (ctx.recovery) stmts.push(`${ind(ctx)}const ${mySyncV} = _ctx._sync`);
5798
- if (def.min === 1) {
6000
+ for (let i = 0; i < def.min; i++) {
5799
6001
  const firstR = emit(def.parser, ctx, curV);
5800
6002
  stmts.push(...firstR.stmts);
5801
6003
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
5802
6004
  stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
5803
6005
  }
6006
+ const maxV = def.max === void 0 ? null : wantValue ? `${arrV}.length` : v(ctx, "_cnt");
6007
+ if (maxV !== null && !wantValue) stmts.push(`${ind(ctx)}let ${maxV} = ${def.min}`);
5804
6008
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
5805
6009
  ctx.indent++;
6010
+ if (maxV !== null) stmts.push(`${ind(ctx)}if (${maxV} >= ${def.max}) break`);
5806
6011
  let itemPos = curV;
5807
6012
  let rollback = "";
5808
6013
  if (ctx.activeTrivia) {
@@ -5843,7 +6048,7 @@ function emitMany(def, ctx, pos) {
5843
6048
  `${ind(ctx)}if (!${iterOk}) {`,
5844
6049
  `${ind(ctx)} ${rollback}if (_ctx._tolerant && ${mySyncV} !== undefined && !_ctx._rec.at(${mySyncV}, input, ${itemPos}, _ctx)) {`,
5845
6050
  `${ind(ctx)} const ${rrV} = _ctx._rec.scan(input, ${itemPos}, _ctx, ${mySyncV}, ${exp})`,
5846
- ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : [],
6051
+ ...wantValue ? [`${ind(ctx)} ${arrV}.push(${rrV}.error)`] : maxV !== null ? [`${ind(ctx)} ${maxV}++`] : [],
5847
6052
  `${ind(ctx)} _ctx._rec.capture(_ctx, ${rrV}.error)`,
5848
6053
  `${ind(ctx)} ${curV} = ${rrV}.end`,
5849
6054
  `${ind(ctx)} continue`,
@@ -5856,6 +6061,7 @@ function emitMany(def, ctx, pos) {
5856
6061
  stmts.push(`${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`);
5857
6062
  }
5858
6063
  if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
6064
+ else if (maxV !== null) stmts.push(`${ind(ctx)}${maxV}++`);
5859
6065
  stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
5860
6066
  ctx.indent--;
5861
6067
  stmts.push(`${ind(ctx)}}`);
@@ -5920,8 +6126,8 @@ function emitSepBy(_p, def, ctx, pos) {
5920
6126
  ...rec ? [`${ind(ctx)}const ${mySyncV} = _ctx._sync`] : [],
5921
6127
  ...firstStmts
5922
6128
  ];
5923
- const failItem = (nextOkVar, itemPosVar, rb, originalFail) => {
5924
- if (!rec) return [originalFail];
6129
+ const failItem = (nextOkVar, itemPosVar, breakStmt) => {
6130
+ if (!rec) return [`${ind(ctx)}if (!${nextOkVar}) ${breakStmt === "break" ? "break" : `{ ${breakStmt} }`}`];
5925
6131
  const rr = v(ctx, "_rr");
5926
6132
  return [
5927
6133
  `${ind(ctx)}if (!${nextOkVar}) {`,
@@ -5929,10 +6135,24 @@ function emitSepBy(_p, def, ctx, pos) {
5929
6135
  `${ind(ctx)} const ${rr} = _ctx._rec.scan(input, ${itemPosVar}, _ctx, ${scanSync}, ${exp})`,
5930
6136
  `${ind(ctx)} ${arrV}.push(${rr}.error); _ctx._rec.capture(_ctx, ${rr}.error); ${curV} = ${rr}.end; continue`,
5931
6137
  `${ind(ctx)} }`,
5932
- `${ind(ctx)} ${rb}break`,
6138
+ `${ind(ctx)} ${breakStmt}`,
5933
6139
  `${ind(ctx)}}`
5934
6140
  ];
5935
6141
  };
6142
+ const itemFailBreak = (sepEndVar, sepRb, postSepRb) => def.trailing === void 0 ? `${sepRb}break` : `${postSepRb}${curV} = ${sepEndVar}; break`;
6143
+ const postSepMarks = () => {
6144
+ if (def.trailing === void 0 || !ctx.capturing) return { decl: [], rb: "" };
6145
+ const lv = v(ctx, "_tlv"), rw = v(ctx, "_trw"), tl = v(ctx, "_ttl"), lg = v(ctx, "_tlg");
6146
+ return {
6147
+ decl: [
6148
+ `${ind(ctx)}const ${lv} = _ctx._cstLeaves ? _ctx._cstLeaves.length : 0`,
6149
+ `${ind(ctx)}const ${rw} = _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0`,
6150
+ `${ind(ctx)}const ${tl} = _ctx._cstTriviaLog ? _ctx._cstTriviaLog.length : 0`,
6151
+ `${ind(ctx)}const ${lg} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`
6152
+ ],
6153
+ 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}; `
6154
+ };
6155
+ };
5936
6156
  if (rec) {
5937
6157
  const rr0 = v(ctx, "_rr0");
5938
6158
  const didV = v(ctx, "_did");
@@ -5958,6 +6178,7 @@ function emitSepBy(_p, def, ctx, pos) {
5958
6178
  );
5959
6179
  ctx.indent++;
5960
6180
  }
6181
+ if (def.max !== void 0) stmts.push(`${ind(ctx)}if (${arrV}.length >= ${def.max}) break`);
5961
6182
  let sepAtPos = curV;
5962
6183
  if (ctx.activeTrivia) {
5963
6184
  if (ctx.capturing) {
@@ -5978,12 +6199,14 @@ function emitSepBy(_p, def, ctx, pos) {
5978
6199
  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}; `;
5979
6200
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
5980
6201
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) { ${rollbackToSep}break }`);
6202
+ const post = postSepMarks();
6203
+ stmts.push(...post.decl);
5981
6204
  const npV = v(ctx, "_np");
5982
6205
  stmts.push(`${ind(ctx)}const ${npV} = ${capFn}(input, ${sepEnd}, _ctx, 1)`);
5983
6206
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
5984
6207
  stmts.push(
5985
6208
  ...nextStmts,
5986
- ...failItem(nextOk, npV, rollbackToSep, `${ind(ctx)}if (!${nextOk}) { ${rollbackToSep}break }`),
6209
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, rollbackToSep, post.rb)),
5987
6210
  `${ind(ctx)}${arrV}.push(${nextVal})`,
5988
6211
  `${ind(ctx)}${curV} = ${nextEnd}`
5989
6212
  );
@@ -5999,7 +6222,7 @@ function emitSepBy(_p, def, ctx, pos) {
5999
6222
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, npV, true);
6000
6223
  stmts.push(
6001
6224
  ...nextStmts,
6002
- ...failItem(nextOk, npV, "", `${ind(ctx)}if (!${nextOk}) break`),
6225
+ ...failItem(nextOk, npV, itemFailBreak(sepEnd, "", "")),
6003
6226
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6004
6227
  `${ind(ctx)}${curV} = ${nextEnd}`
6005
6228
  );
@@ -6016,10 +6239,12 @@ function emitSepBy(_p, def, ctx, pos) {
6016
6239
  const { stmts: sepStmts, okVar: sepOk, endVar: sepEnd } = emitFallible(def.separator, ctx, sepAtPos, true);
6017
6240
  stmts.push(...sepStmts, `${ind(ctx)}if (!${sepOk}) break`);
6018
6241
  const nextRb = markLv ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLv}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRw}; ` : "";
6242
+ const post = postSepMarks();
6243
+ stmts.push(...post.decl);
6019
6244
  const { stmts: nextStmts, okVar: nextOk, valVar: nextVal, endVar: nextEnd } = emitFallible(def.parser, ctx, sepEnd, true);
6020
6245
  stmts.push(
6021
6246
  ...nextStmts,
6022
- ...failItem(nextOk, sepEnd, nextRb, `${ind(ctx)}if (!${nextOk}) { ${nextRb}break }`),
6247
+ ...failItem(nextOk, sepEnd, itemFailBreak(sepEnd, nextRb, post.rb)),
6023
6248
  `${ind(ctx)}${arrV}.push(${nextVal})`,
6024
6249
  `${ind(ctx)}${curV} = ${nextEnd}`
6025
6250
  );
@@ -6028,6 +6253,9 @@ function emitSepBy(_p, def, ctx, pos) {
6028
6253
  stmts.push(`${ind(ctx)}}`);
6029
6254
  ctx.indent--;
6030
6255
  stmts.push(`${ind(ctx)}}`);
6256
+ if (def.min >= 1) {
6257
+ stmts.push(...emitIfFail(ctx, `${arrV}.length < ${def.min}`, failArrBody(ctx, deriveExpectedArr([def.parser]), curV)));
6258
+ }
6031
6259
  return { stmts, valueVar: arrV, endVar: curV };
6032
6260
  }
6033
6261
  function emitScanTo(def, ctx, pos) {
@@ -6079,17 +6307,44 @@ function emitScanTo(def, ctx, pos) {
6079
6307
  return { stmts, valueVar: valV, endVar: curV };
6080
6308
  }
6081
6309
  function emitNot(def, ctx, pos) {
6310
+ const sinksLive = !!ctx.capturing || !!ctx.recovery;
6311
+ const leaves = sinksLive ? v(ctx, "_ntl") : null;
6312
+ const raw = sinksLive ? v(ctx, "_ntr") : null;
6313
+ const tl = sinksLive ? v(ctx, "_ntt") : null;
6314
+ const log = sinksLive ? v(ctx, "_ntg") : null;
6315
+ const fields = sinksLive ? v(ctx, "_ntf") : null;
6316
+ const errors = sinksLive ? v(ctx, "_nte") : null;
6082
6317
  const { stmts, okVar } = emitFallible(def.parser, ctx, pos, true);
6083
6318
  const label2 = JSON.stringify(`not(${def.parser._tag})`);
6084
6319
  return {
6085
6320
  stmts: [
6321
+ ...sinksLive ? [
6322
+ `${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`
6323
+ ] : [],
6086
6324
  ...stmts,
6325
+ ...sinksLive ? [
6326
+ `${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}`
6327
+ ] : [],
6087
6328
  ...emitIfFail(ctx, okVar, failBody(ctx, label2, pos))
6088
6329
  ],
6089
6330
  valueVar: "null",
6090
6331
  endVar: pos
6091
6332
  };
6092
6333
  }
6334
+ function emitPeek(def, ctx, pos) {
6335
+ const probeCtx = { ...ctx, capturing: false, noHoist: true };
6336
+ const { stmts, okVar } = emitFallible(def.parser, probeCtx, pos, true);
6337
+ ctx.vars = probeCtx.vars;
6338
+ const label2 = JSON.stringify(`peek(${def.parser._tag})`);
6339
+ return {
6340
+ stmts: [
6341
+ ...stmts,
6342
+ ...emitIfFail(ctx, `!${okVar}`, failBody(ctx, label2, pos))
6343
+ ],
6344
+ valueVar: "null",
6345
+ endVar: pos
6346
+ };
6347
+ }
6093
6348
  function escapeTokenLiteral(value) {
6094
6349
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6095
6350
  }
@@ -6377,9 +6632,9 @@ function emitRuntimeFallback(parser2, ctx, pos) {
6377
6632
  return { stmts, valueVar: vv, endVar: ev };
6378
6633
  }
6379
6634
  function emitLazy(p, def, ctx, pos) {
6380
- const canonical = ctx.ruleNames?.get(p);
6635
+ const canonical2 = ctx.ruleNames?.get(p);
6381
6636
  const usage = ctx.lazyUsage;
6382
- if (!canonical && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6637
+ if (!canonical2 && usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
6383
6638
  let resolved;
6384
6639
  try {
6385
6640
  resolved = def.thunk();
@@ -6389,7 +6644,7 @@ function emitLazy(p, def, ctx, pos) {
6389
6644
  return emit(resolved, ctx, pos);
6390
6645
  }
6391
6646
  if (!ctx.namedParsers.has(p)) {
6392
- const fnName2 = canonical ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6647
+ const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
6393
6648
  ctx.namedParsers.set(p, fnName2);
6394
6649
  let resolved;
6395
6650
  try {
@@ -6672,6 +6927,8 @@ function emitDispatch(p, ctx, pos) {
6672
6927
  }
6673
6928
  case "not":
6674
6929
  return emitNot(def, ctx, pos);
6930
+ case "peek":
6931
+ return emitPeek(def, ctx, pos);
6675
6932
  case "node":
6676
6933
  return emitNode(def, ctx, pos);
6677
6934
  case "scanTo":
@@ -6760,6 +7017,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
6760
7017
  case "many":
6761
7018
  case "oneOrMore":
6762
7019
  case "not":
7020
+ case "peek":
6763
7021
  case "transform":
6764
7022
  return hasNodeDef(d.parser, seen);
6765
7023
  case "skip":
@@ -6811,17 +7069,18 @@ function childrenOf(def) {
6811
7069
  case "field":
6812
7070
  case "grammar":
6813
7071
  case "not":
7072
+ case "peek":
6814
7073
  case "node":
6815
7074
  case "withCtx":
6816
7075
  case "expect":
6817
7076
  return [def.parser];
6818
- // emitMany's oneOrMore branch and emitSepBy both codegen `def.parser` TWICE
6819
- // (a mandatory first match, then again inside the repeat loop) — two real
6820
- // emit() call sites, so the usage analysis must see two edges here or it
7077
+ // emitMany's min>=1 branch and emitSepBy both codegen `def.parser` more than
7078
+ // once (`min` mandatory matches, then again inside the repeat loop) — each is a
7079
+ // real emit() call site, so the usage analysis must see one edge per site or it
6821
7080
  // undercounts a single-use `parser` ref as inline-safe when it's actually
6822
- // referenced from two positions within this one compiled function.
7081
+ // referenced from several positions within this one compiled function.
6823
7082
  case "oneOrMore":
6824
- return [def.parser, def.parser];
7083
+ return Array.from({ length: def.min + 1 }, () => def.parser);
6825
7084
  case "sepBy":
6826
7085
  return [def.parser, def.parser, def.separator];
6827
7086
  case "skip":
@@ -6967,12 +7226,41 @@ function resolveGatingLevel(opt) {
6967
7226
  return "warn";
6968
7227
  }
6969
7228
  function runGatingDiagnostic(combinator, opt) {
7229
+ return reportGating(opt, (o) => analyzeGating(combinator, o));
7230
+ }
7231
+ function runGatingDiagnosticRules(ruleMap, opt) {
7232
+ return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7233
+ }
7234
+ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7235
+ if (resolveGatingLevel(opt) === "off") return void 0;
7236
+ const ruleMaps = getRuleMaps();
7237
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
7238
+ const winners = /* @__PURE__ */ new Map();
7239
+ for (const map of ruleMaps) for (const [name, rule] of map) {
7240
+ if (rule._def.tag === "lazy") {
7241
+ try {
7242
+ rule._def.thunk();
7243
+ } catch {
7244
+ continue;
7245
+ }
7246
+ }
7247
+ winners.set(name, rule);
7248
+ }
7249
+ const entries = [...winners];
7250
+ return reportGating(opt, (o) => {
7251
+ const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7252
+ const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7253
+ return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
7254
+ });
7255
+ }
7256
+ function reportGating(opt, analyze) {
6970
7257
  const level = resolveGatingLevel(opt);
6971
7258
  if (level === "off") return void 0;
6972
- const accept = opt !== null && typeof opt === "object" ? opt.accept : void 0;
7259
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
7260
+ 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;
6973
7261
  let report;
6974
7262
  try {
6975
- report = analyzeGating(combinator, accept ? { accept } : void 0);
7263
+ report = analyze(analyzeOpts);
6976
7264
  } catch {
6977
7265
  return void 0;
6978
7266
  }
@@ -7108,6 +7396,7 @@ function publicRuleWrapperSource(rule, fnSource) {
7108
7396
  return `Object.assign(${fnSource}, { _meta: { triviaKindLabels: ${JSON.stringify(labels)} } })`;
7109
7397
  }
7110
7398
  function compileRuleMap(ruleMap, opts) {
7399
+ runGatingDiagnosticRules(ruleMap, opts?.gating);
7111
7400
  for (const [, rule] of ruleMap) markUnusedValues(rule);
7112
7401
  const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
7113
7402
  const grammarTrivia = opts?.trivia;
@@ -7226,6 +7515,7 @@ function compileRuleMap(ruleMap, opts) {
7226
7515
  }
7227
7516
  function compileLinkable(ruleMapArg, ns, opts) {
7228
7517
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7518
+ if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
7229
7519
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7230
7520
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7231
7521
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
@@ -7282,6 +7572,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7282
7572
  fnNameToKey.set(fn, key);
7283
7573
  }
7284
7574
  ctx.ruleNames = ruleNames;
7575
+ const externalRefs = /* @__PURE__ */ new Set();
7285
7576
  {
7286
7577
  const scanned = /* @__PURE__ */ new Set();
7287
7578
  const scanExternal = (p) => {
@@ -7297,11 +7588,14 @@ function compileLinkable(ruleMapArg, ns, opts) {
7297
7588
  }
7298
7589
  if (resolved === void 0) {
7299
7590
  const name = p._ruleName;
7300
- if (name && !ctx.namedParsers.has(p)) {
7301
- assertRuleName(name);
7302
- const fn = `_r_${name}`;
7303
- ruleNames.set(p, fn);
7304
- ctx.namedParsers.set(p, fn);
7591
+ if (name) {
7592
+ externalRefs.add(p);
7593
+ if (!ctx.namedParsers.has(p)) {
7594
+ assertRuleName(name);
7595
+ const fn = `_r_${name}`;
7596
+ ruleNames.set(p, fn);
7597
+ ctx.namedParsers.set(p, fn);
7598
+ }
7305
7599
  }
7306
7600
  return;
7307
7601
  }
@@ -7414,12 +7708,32 @@ function compileLinkable(ruleMapArg, ns, opts) {
7414
7708
  needsEmptyTl: !!ctx.needsEmptyTl,
7415
7709
  needsHostReads: !!ctx.needsHostReads,
7416
7710
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7417
- isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule)),
7711
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7418
7712
  mfFns: mfSrcs ? [] : ctx.mapFns,
7419
7713
  buildFns: buildSrcs ? [] : ctx.buildFns
7420
7714
  };
7421
7715
  }
7422
- function hasSemanticReduction(roots) {
7716
+ function hasExternalRuleRef(ruleMap) {
7717
+ const seen = /* @__PURE__ */ new Set();
7718
+ const visit = (p) => {
7719
+ if (seen.has(p)) return false;
7720
+ seen.add(p);
7721
+ const def = p._def;
7722
+ if (def.tag === "lazy") {
7723
+ let resolved;
7724
+ try {
7725
+ resolved = def.thunk();
7726
+ } catch {
7727
+ resolved = void 0;
7728
+ }
7729
+ if (resolved === void 0) return p._ruleName !== void 0;
7730
+ return visit(resolved);
7731
+ }
7732
+ return childrenOf(def).some(visit);
7733
+ };
7734
+ return ruleMap.some(([, rule]) => visit(rule));
7735
+ }
7736
+ function hasSemanticReduction(roots, externalRefs) {
7423
7737
  const seen = /* @__PURE__ */ new Set();
7424
7738
  const visit = (parser2) => {
7425
7739
  if (seen.has(parser2)) return false;
@@ -7430,6 +7744,7 @@ function hasSemanticReduction(roots) {
7430
7744
  if (def.tag === "guard" || def.tag === "withCtx") return true;
7431
7745
  if (def.tag === "node" && def.build !== void 0) return true;
7432
7746
  if (def.tag === "lazy") {
7747
+ if (externalRefs?.has(parser2)) return false;
7433
7748
  try {
7434
7749
  return visit(def.thunk());
7435
7750
  } catch {
@@ -7521,6 +7836,7 @@ function childrenOf2(def) {
7521
7836
  case "label":
7522
7837
  case "field":
7523
7838
  case "not":
7839
+ case "peek":
7524
7840
  case "node":
7525
7841
  case "withCtx":
7526
7842
  case "expect":
@@ -7554,9 +7870,16 @@ function lazyTarget(c) {
7554
7870
  }
7555
7871
  var Unserializable = class extends Error {
7556
7872
  };
7557
- function serializeRuleMap(ruleMap) {
7873
+ function repeatOpts(min, max, trailing) {
7874
+ const parts = [];
7875
+ if (min !== 0) parts.push(`min: ${min}`);
7876
+ if (max !== void 0) parts.push(`max: ${max}`);
7877
+ if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
7878
+ return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
7879
+ }
7880
+ function serializeRuleMap(ruleMap, scanSkip) {
7558
7881
  try {
7559
- return new Serializer(ruleMap).run();
7882
+ return new Serializer(ruleMap, scanSkip ?? []).run();
7560
7883
  } catch (e) {
7561
7884
  if (e instanceof Unserializable) {
7562
7885
  if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${e.message}`);
@@ -7657,6 +7980,7 @@ function evalRuleMapIR(ir) {
7657
7980
  "optional",
7658
7981
  "sepBy",
7659
7982
  "not",
7983
+ "peek",
7660
7984
  "node",
7661
7985
  "parser",
7662
7986
  "scanTo",
@@ -7689,6 +8013,7 @@ function evalRuleMapIR(ir) {
7689
8013
  optional,
7690
8014
  sepBy,
7691
8015
  not,
8016
+ peek,
7692
8017
  node,
7693
8018
  parser,
7694
8019
  scanTo,
@@ -7717,10 +8042,28 @@ var Serializer = class {
7717
8042
  decls = [];
7718
8043
  ruleValues = /* @__PURE__ */ new Set();
7719
8044
  ruleMap;
7720
- constructor(ruleMap) {
8045
+ scanSkip;
8046
+ constructor(ruleMap, scanSkip = []) {
7721
8047
  this.ruleMap = ruleMap;
8048
+ this.scanSkip = scanSkip;
7722
8049
  for (const [, c] of ruleMap) this.ruleValues.add(this.body(c));
7723
8050
  }
8051
+ /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
8052
+ * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
8053
+ * serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
8054
+ * outside the `(g) => …` factory where `g` is bound. A unit that does reference a
8055
+ * rule can't be carried in options; bail to the full-pieces fallback. */
8056
+ assertNoRuleRef(c, seen) {
8057
+ if (seen.has(c)) return;
8058
+ seen.add(c);
8059
+ if (c._def.tag === "lazy") {
8060
+ if (ruleNameOf2(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
8061
+ const target = lazyTarget(c);
8062
+ if (target) this.assertNoRuleRef(target, seen);
8063
+ return;
8064
+ }
8065
+ for (const child of childrenOf2(c._def)) this.assertNoRuleRef(child, seen);
8066
+ }
7724
8067
  /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
7725
8068
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
7726
8069
  * `g[name]` ref — only the top-level entry is unwrapped. */
@@ -7729,21 +8072,38 @@ var Serializer = class {
7729
8072
  }
7730
8073
  run() {
7731
8074
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
8075
+ for (const c of this.scanSkip) {
8076
+ this.assertNoRuleRef(c, /* @__PURE__ */ new Set());
8077
+ this.analyze(c, /* @__PURE__ */ new Set());
8078
+ }
7732
8079
  let n = 0;
7733
8080
  for (const [c, count] of this.counts) {
7734
8081
  if (this.ruleValues.has(c)) continue;
7735
8082
  if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`);
7736
8083
  }
8084
+ const outerDecls = [];
8085
+ const innerDecls = [];
8086
+ let optionsArg = "";
8087
+ if (this.scanSkip.length > 0) {
8088
+ this.decls = outerDecls;
8089
+ const units = this.scanSkip.map((c) => this.ref(c, null));
8090
+ optionsArg = `{ scanSkip: [${units.join(", ")}] }, `;
8091
+ }
8092
+ this.decls = innerDecls;
7737
8093
  const body = this.ruleMap.map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c))}`).join(",\n");
7738
- if (this.decls.length === 0) return `rules((g) => ({
8094
+ const factory = innerDecls.length === 0 ? `(g) => ({
7739
8095
  ${body}
7740
- }))`;
7741
- return `rules((g) => {
7742
- ${this.decls.map((d) => " " + d).join("\n")}
8096
+ })` : `(g) => {
8097
+ ${innerDecls.map((d) => " " + d).join("\n")}
7743
8098
  return ({
7744
8099
  ${body}
7745
8100
  })
7746
- })`;
8101
+ }`;
8102
+ const call = `rules(${optionsArg}${factory})`;
8103
+ return outerDecls.length === 0 ? call : `(() => {
8104
+ ${outerDecls.map((d) => " " + d).join("\n")}
8105
+ return ${call}
8106
+ })()`;
7747
8107
  }
7748
8108
  /** Count identity references and flag self-referential subtrees. `active` is the
7749
8109
  * set of combinators on the current DFS path (to detect a lazy pointing back). */
@@ -7828,17 +8188,21 @@ ${body}
7828
8188
  return `_gch([${items.join(", ")}])`;
7829
8189
  }
7830
8190
  case "many":
7831
- return `many(${kid(def.parser)})`;
8191
+ return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
8192
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
8193
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
7832
8194
  case "oneOrMore":
7833
- return `oneOrMore(${kid(def.parser)})`;
8195
+ return def.min === 1 && def.max === void 0 ? `oneOrMore(${kid(def.parser)})` : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`;
7834
8196
  case "optional":
7835
8197
  return `optional(${kid(def.parser)})`;
7836
8198
  case "attempt":
7837
8199
  return `attempt(${kid(def.parser)})`;
7838
8200
  case "sepBy":
7839
- return `sepBy(${kid(def.parser)}, ${kid(def.separator)})`;
8201
+ return `sepBy(${kid(def.parser)}, ${kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`;
7840
8202
  case "not":
7841
8203
  return `not(${kid(def.parser)})`;
8204
+ case "peek":
8205
+ return `peek(${kid(def.parser)})`;
7842
8206
  case "trivia":
7843
8207
  return `trivia(${kid(def.parser)})`;
7844
8208
  case "token":
@@ -8274,15 +8638,12 @@ var SUPPORTED = {
8274
8638
  literal: (...a) => literal(a[0], a[1]),
8275
8639
  regex: (...a) => regex(a[0], a[1]),
8276
8640
  keywords: (...a) => keywords(a[0], a[1]),
8277
- word: (...a) => word(a[0], a[1]),
8641
+ word: (...a) => word(a[0], a[1], a[2]),
8278
8642
  sequence: (...a) => sequence(...a),
8279
8643
  choice: (...a) => choice(...a),
8280
8644
  attempt: (...a) => attempt(a[0]),
8281
- many: (...a) => many(a[0]),
8282
- oneOrMore: (...a) => oneOrMore(a[0]),
8283
8645
  optional: (...a) => optional(a[0]),
8284
8646
  skip: (...a) => skip(a[0], a[1]),
8285
- sepBy: (...a) => sepBy(a[0], a[1]),
8286
8647
  trivia: (...a) => trivia(a[0]),
8287
8648
  label: (...a) => label(a[0], a[1]),
8288
8649
  field: (...a) => field(a[0], a[1]),
@@ -8458,8 +8819,8 @@ function exprToCombi(node2, scope, code, mfs) {
8458
8819
  return null;
8459
8820
  }
8460
8821
  }
8461
- if (callee.name === "sepBy") {
8462
- const [itemArg, sepArg] = node2.arguments;
8822
+ if (callee.name === "sepBy" || callee.name === "oneOrMoreSep") {
8823
+ const [itemArg, sepArg, optsArg] = node2.arguments;
8463
8824
  if (!itemArg || !sepArg || itemArg.type === "SpreadElement" || sepArg.type === "SpreadElement") return null;
8464
8825
  const itemMfs = [];
8465
8826
  const itemCombi = anyValue(itemArg, scope, code, itemMfs);
@@ -8467,25 +8828,42 @@ function exprToCombi(node2, scope, code, mfs) {
8467
8828
  const sepMfs = [];
8468
8829
  const sepCombi = anyValue(sepArg, scope, code, sepMfs);
8469
8830
  if (!isCombinator(sepCombi)) return null;
8831
+ let opts;
8832
+ if (optsArg) {
8833
+ if (optsArg.type === "SpreadElement") return null;
8834
+ const v2 = anyValue(optsArg, scope, code, []);
8835
+ if (!v2 || typeof v2 !== "object") return null;
8836
+ opts = v2;
8837
+ }
8470
8838
  if (mfs) mfs.push(...itemMfs, ...sepMfs, ...itemMfs);
8471
8839
  try {
8472
- return sepBy(itemCombi, sepCombi);
8840
+ return callee.name === "sepBy" ? sepBy(itemCombi, sepCombi, opts) : oneOrMoreSep(itemCombi, sepCombi, opts);
8473
8841
  } catch {
8474
8842
  return null;
8475
8843
  }
8476
8844
  }
8477
- if (callee.name === "oneOrMore") {
8478
- const [itemArg] = node2.arguments;
8845
+ if (callee.name === "oneOrMore" || callee.name === "many") {
8846
+ const [itemArg, optsArg] = node2.arguments;
8479
8847
  if (!itemArg || itemArg.type === "SpreadElement") return null;
8480
8848
  const itemMfs = [];
8481
8849
  const itemCombi = anyValue(itemArg, scope, code, itemMfs);
8482
8850
  if (!isCombinator(itemCombi)) return null;
8483
- if (mfs) mfs.push(...itemMfs, ...itemMfs);
8851
+ let opts;
8852
+ if (optsArg) {
8853
+ if (optsArg.type === "SpreadElement") return null;
8854
+ const v2 = anyValue(optsArg, scope, code, []);
8855
+ if (!v2 || typeof v2 !== "object") return null;
8856
+ opts = v2;
8857
+ }
8858
+ let combi;
8484
8859
  try {
8485
- return oneOrMore(itemCombi);
8860
+ combi = callee.name === "many" ? many(itemCombi, opts) : oneOrMore(itemCombi, opts);
8486
8861
  } catch {
8487
8862
  return null;
8488
8863
  }
8864
+ const min = combi._def.tag === "oneOrMore" ? combi._def.min : 0;
8865
+ if (mfs) for (let i = 0; i <= min; i++) mfs.push(...itemMfs);
8866
+ return combi;
8489
8867
  }
8490
8868
  if (callee.name === "not") {
8491
8869
  const [innerArg] = node2.arguments;
@@ -8498,6 +8876,17 @@ function exprToCombi(node2, scope, code, mfs) {
8498
8876
  return null;
8499
8877
  }
8500
8878
  }
8879
+ if (callee.name === "peek") {
8880
+ const [innerArg] = node2.arguments;
8881
+ if (!innerArg || innerArg.type === "SpreadElement") return null;
8882
+ const inner = anyValue(innerArg, scope, code, mfs);
8883
+ if (!isCombinator(inner)) return null;
8884
+ try {
8885
+ return peek(inner);
8886
+ } catch {
8887
+ return null;
8888
+ }
8889
+ }
8501
8890
  if (callee.name === "balanced") {
8502
8891
  const [openArg, closeArg, optsArg] = node2.arguments;
8503
8892
  if (!openArg || !closeArg || openArg.type === "SpreadElement" || closeArg.type === "SpreadElement") return null;
@@ -9127,6 +9516,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9127
9516
  const warnings = [];
9128
9517
  beginLoweringCapture();
9129
9518
  let anyUnresolved = false;
9519
+ let keepMacroImport = false;
9130
9520
  let runtimeComposeFallback = false;
9131
9521
  let ruleMapHolderCounter = 0;
9132
9522
  const lineOf = (pos) => {
@@ -9161,17 +9551,21 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9161
9551
  const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
9162
9552
  const scanSkipValue = optionValue("scanSkip");
9163
9553
  const gScanSkip = scanSkipValue ? evaluateCombinatorArray(scanSkipValue, scope, code) ?? void 0 : void 0;
9554
+ if (gScanSkip) {
9555
+ for (const rule of ruleMap.values()) {
9556
+ if (rule && !rule._meta.isTrivia) {
9557
+ ;
9558
+ rule._meta.grammarScanSkip = gScanSkip;
9559
+ }
9560
+ }
9561
+ }
9164
9562
  return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
9165
9563
  };
9166
9564
  const compileRulesFactory = (init, label2) => {
9167
9565
  const evaluated = evaluateRulesFactory(init, label2);
9168
9566
  if (!evaluated) return null;
9169
9567
  const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
9170
- if (!compiled) {
9171
- warn(init.start, `${label2}: rule map couldn't be inlined`);
9172
- return null;
9173
- }
9174
- return { replacement: compiled.replacement, ruleMap: evaluated.ruleMap, ...compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
9568
+ return { replacement: compiled?.replacement ?? null, ruleMap: evaluated.ruleMap, ...compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
9175
9569
  };
9176
9570
  const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
9177
9571
  const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
@@ -9266,6 +9660,17 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9266
9660
  }
9267
9661
  return out;
9268
9662
  };
9663
+ const carriedRuleMaps = (items) => {
9664
+ const out = [];
9665
+ const add = (it) => {
9666
+ if (isIR(it)) out.push(evalRuleMapIR(it.ir));
9667
+ };
9668
+ for (const it of items) {
9669
+ if (isSpread(it)) for (const p of importedPieces(it.__spreadLocal) ?? []) add(p);
9670
+ else add(it);
9671
+ }
9672
+ return out;
9673
+ };
9269
9674
  const materializeLeafCoverage = (items, localRules, localNs, composing, captureTerminals = false) => {
9270
9675
  const sources = [];
9271
9676
  const add = (item) => {
@@ -9297,12 +9702,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9297
9702
  }
9298
9703
  return pieces;
9299
9704
  };
9300
- const localCarried = (rm, label2, composing) => {
9705
+ const localCarried = (rm, label2, composing, scanSkip) => {
9301
9706
  const entries = [...rm];
9302
9707
  const ns = nsFor(label2);
9303
- const ir = serializeRuleMap(entries);
9708
+ const ir = serializeRuleMap(entries, scanSkip);
9304
9709
  if (ir) return { carried: [{ ns, ir }] };
9305
- const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, recovery });
9710
+ const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
9306
9711
  return p ? { carried: [p] } : null;
9307
9712
  };
9308
9713
  const argPieces = (arg, label2, composing) => {
@@ -9327,18 +9732,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9327
9732
  }
9328
9733
  }
9329
9734
  if (isRulesCall(arg)) {
9330
- const rulesArgs = arg.arguments;
9331
- const a0 = rulesArgs[0];
9332
- const a1 = rulesArgs[1];
9333
- const optionsFirst = a0?.type === "ObjectExpression";
9334
- const factory = optionsFirst ? a1 : a0;
9335
- const rm = factory ? evaluateParserFactory(factory, scope, code, []) : null;
9336
- return rm ? localCarried(rm, label2, composing) : null;
9735
+ const evaluated = evaluateRulesFactory(arg, label2);
9736
+ return evaluated ? localCarried(evaluated.ruleMap, label2, composing, evaluated.scanSkip) : null;
9337
9737
  }
9338
9738
  if (arg.type === "Identifier") {
9339
9739
  const name = arg.name;
9340
9740
  const rm = localRuleMaps.get(name);
9341
- if (rm) return localCarried(rm, label2, composing);
9741
+ if (rm) return localCarried(rm, label2, composing, localGrammarScanSkip.get(name));
9342
9742
  const composed = localComposedCarried.get(name);
9343
9743
  if (composed) return { carried: composed };
9344
9744
  const pieces = importedPieces(name);
@@ -9404,6 +9804,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9404
9804
  }
9405
9805
  carried.push(...r.carried);
9406
9806
  }
9807
+ runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
9407
9808
  const pieces = materializeCarried(carried, composing);
9408
9809
  try {
9409
9810
  return { replacement: emitFusedSource(pieces), carried, ...composing ? { trivia: composing } : {} };
@@ -9452,6 +9853,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9452
9853
  }
9453
9854
  try {
9454
9855
  const localNs = nsFor(`composeLeaf${init.start}`);
9856
+ runFusedGatingDiagnostic(() => [...carriedRuleMaps(carried), [...localRules]]);
9455
9857
  const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
9456
9858
  if (!plainLocalPiece) {
9457
9859
  warn(init.start, "composeLeaf(): local rules could not be statically compiled");
@@ -9540,7 +9942,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9540
9942
  const refEntry = scope.get(varName);
9541
9943
  const refCombi = refEntry?.combi ?? null;
9542
9944
  if (refCombi) {
9543
- const compiled2 = compile(refCombi, void 0, { recovery, coverage: grammarCoverage });
9945
+ const compiled2 = compile(refCombi, void 0, { recovery, coverage: grammarCoverage, gating: { entryName: varName } });
9544
9946
  if (compiled2.inlineExpression === null) {
9545
9947
  warn(init.start, `"${varName}" is a ref() that couldn't be inlined (was .define() called with a static combinator?)`);
9546
9948
  continue;
@@ -9555,17 +9957,26 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9555
9957
  localRuleMaps.set(varName, compiledRules.ruleMap);
9556
9958
  if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia);
9557
9959
  if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip);
9960
+ const ns = nsFor(varName);
9961
+ const pieces = exportPrefix || compiledRules.replacement === null ? compileLinkable([...compiledRules.ruleMap], ns, {
9962
+ ...compiledRules.scanSkip ? { scanSkip: compiledRules.scanSkip } : {},
9963
+ recovery
9964
+ }) : null;
9965
+ if (compiledRules.replacement === null) {
9966
+ if (!pieces || !hasExternalRuleRef([...compiledRules.ruleMap])) {
9967
+ warn(init.start, `${varName}: rule map couldn't be inlined`);
9968
+ continue;
9969
+ }
9970
+ keepMacroImport = true;
9971
+ }
9972
+ const source = compiledRules.replacement ?? code.slice(init.start, init.end);
9558
9973
  let replacement = withCoverageDefinitions(
9559
- compiledRules.replacement,
9560
- compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(compiledRules.replacement)
9974
+ source,
9975
+ compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(source)
9561
9976
  );
9562
- if (exportPrefix) {
9563
- const ns = nsFor(varName);
9564
- const pieces = compileLinkable([...compiledRules.ruleMap], ns, { recovery });
9565
- if (pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
9566
- const ir = serializeRuleMap([...compiledRules.ruleMap]);
9567
- replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
9568
- }
9977
+ if (exportPrefix && pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
9978
+ const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
9979
+ replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
9569
9980
  }
9570
9981
  replacements.push({ start: init.start, end: init.end, replacement });
9571
9982
  continue;
@@ -9608,7 +10019,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9608
10019
  warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`);
9609
10020
  continue;
9610
10021
  }
9611
- const compiled = compile(parser2, void 0, { recovery, coverage: grammarCoverage });
10022
+ const compiled = compile(parser2, void 0, { recovery, coverage: grammarCoverage, gating: { entryName: varName } });
9612
10023
  if (compiled.inlineExpression === null) {
9613
10024
  warn(init.start, `"${varName}" couldn't be inlined (likely closes over a runtime value)`);
9614
10025
  continue;
@@ -9627,6 +10038,10 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9627
10038
  }
9628
10039
  const compiledRules = compileRulesFactory(init, "{ \u2026 }");
9629
10040
  if (!compiledRules) continue;
10041
+ if (compiledRules.replacement === null) {
10042
+ warn(init.start, `{ \u2026 }: rule map couldn't be inlined`);
10043
+ continue;
10044
+ }
9630
10045
  const pattern = d.id;
9631
10046
  const bindings = [];
9632
10047
  let allOk = true;
@@ -9668,7 +10083,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9668
10083
  }
9669
10084
  if (replacements.length === 0 && warnings.length === 0) return null;
9670
10085
  for (const imp of macroImports) {
9671
- imp.fullyResolved = !anyUnresolved;
10086
+ imp.fullyResolved = !anyUnresolved && !keepMacroImport;
9672
10087
  }
9673
10088
  const ms = new MagicString(code);
9674
10089
  if (!anyUnresolved) {