parseman 0.45.0 → 0.47.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 (333) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-gate.d.ts +158 -0
  3. package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
  4. package/dist/analysis/choice-cost-render.d.ts +96 -0
  5. package/dist/analysis/choice-cost-render.d.ts.map +1 -0
  6. package/dist/analysis/choice-cost.d.ts +383 -0
  7. package/dist/analysis/choice-cost.d.ts.map +1 -0
  8. package/dist/analysis/commitment.d.ts +131 -0
  9. package/dist/analysis/commitment.d.ts.map +1 -0
  10. package/dist/analysis/corpus.d.ts +73 -0
  11. package/dist/analysis/corpus.d.ts.map +1 -0
  12. package/dist/analysis/diagnose-render.d.ts +65 -0
  13. package/dist/analysis/diagnose-render.d.ts.map +1 -0
  14. package/dist/analysis/diagnose.d.ts +15 -0
  15. package/dist/analysis/diagnose.d.ts.map +1 -1
  16. package/dist/analysis/diagnostics.cjs +13736 -0
  17. package/dist/analysis/diagnostics.cjs.map +6 -0
  18. package/dist/analysis/diagnostics.d.ts +27 -0
  19. package/dist/analysis/diagnostics.d.ts.map +1 -0
  20. package/dist/analysis/diagnostics.js +13713 -0
  21. package/dist/analysis/diagnostics.js.map +6 -0
  22. package/dist/analysis/duplication.d.ts.map +1 -1
  23. package/dist/analysis/fix-render.d.ts +43 -0
  24. package/dist/analysis/fix-render.d.ts.map +1 -0
  25. package/dist/analysis/fix.d.ts +172 -0
  26. package/dist/analysis/fix.d.ts.map +1 -0
  27. package/dist/analysis/format-number.d.ts +23 -0
  28. package/dist/analysis/format-number.d.ts.map +1 -0
  29. package/dist/analysis/gating.d.ts +47 -2
  30. package/dist/analysis/gating.d.ts.map +1 -1
  31. package/dist/analysis/rebuild.d.ts +61 -0
  32. package/dist/analysis/rebuild.d.ts.map +1 -0
  33. package/dist/analysis/terminal.d.ts +145 -0
  34. package/dist/analysis/terminal.d.ts.map +1 -0
  35. package/dist/cli/index.cjs +14837 -0
  36. package/dist/cli/index.cjs.map +6 -0
  37. package/dist/cli/index.d.ts +3 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +14814 -0
  40. package/dist/cli/index.js.map +6 -0
  41. package/dist/combinators/adjacency.d.ts +100 -0
  42. package/dist/combinators/adjacency.d.ts.map +1 -0
  43. package/dist/combinators/choice.d.ts.map +1 -1
  44. package/dist/combinators/completions.d.ts.map +1 -1
  45. package/dist/combinators/dispatch.d.ts +8 -1
  46. package/dist/combinators/dispatch.d.ts.map +1 -1
  47. package/dist/combinators/expect.d.ts.map +1 -1
  48. package/dist/combinators/first-set.d.ts.map +1 -1
  49. package/dist/combinators/gate.d.ts.map +1 -1
  50. package/dist/combinators/grammar.d.ts +22 -0
  51. package/dist/combinators/grammar.d.ts.map +1 -1
  52. package/dist/combinators/literal.d.ts +8 -0
  53. package/dist/combinators/literal.d.ts.map +1 -1
  54. package/dist/combinators/map.d.ts +0 -1
  55. package/dist/combinators/map.d.ts.map +1 -1
  56. package/dist/combinators/node.d.ts +13 -3
  57. package/dist/combinators/node.d.ts.map +1 -1
  58. package/dist/combinators/parser.d.ts +1 -1
  59. package/dist/combinators/parser.d.ts.map +1 -1
  60. package/dist/combinators/ref.d.ts.map +1 -1
  61. package/dist/combinators/regex.d.ts +1 -16
  62. package/dist/combinators/regex.d.ts.map +1 -1
  63. package/dist/combinators/repeat.d.ts +30 -2
  64. package/dist/combinators/repeat.d.ts.map +1 -1
  65. package/dist/combinators/scanTo.d.ts +71 -1
  66. package/dist/combinators/scanTo.d.ts.map +1 -1
  67. package/dist/combinators/sequence.d.ts.map +1 -1
  68. package/dist/combinators/token.d.ts.map +1 -1
  69. package/dist/combinators/trivia-boundary.d.ts +26 -0
  70. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  71. package/dist/combinators/trivia-skip.d.ts +40 -1
  72. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  73. package/dist/combinators/withCtx.d.ts.map +1 -1
  74. package/dist/compiler/build-arity.d.ts.map +1 -1
  75. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  76. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  77. package/dist/compiler/degradation.d.ts +0 -2
  78. package/dist/compiler/degradation.d.ts.map +1 -1
  79. package/dist/compiler/fields.d.ts +13 -0
  80. package/dist/compiler/fields.d.ts.map +1 -1
  81. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  82. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  83. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  84. package/dist/compiler/linker.d.ts +79 -47
  85. package/dist/compiler/linker.d.ts.map +1 -1
  86. package/dist/compiler/token-alphabet.d.ts +81 -0
  87. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  88. package/dist/compiler/token-dispatch.d.ts +95 -0
  89. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  90. package/dist/compiler/token-scanner.d.ts +53 -0
  91. package/dist/compiler/token-scanner.d.ts.map +1 -0
  92. package/dist/compiler/value-usage.d.ts.map +1 -1
  93. package/dist/coverage.d.ts +1 -1
  94. package/dist/coverage.d.ts.map +1 -1
  95. package/dist/cst/capture-buffer.d.ts +35 -0
  96. package/dist/cst/capture-buffer.d.ts.map +1 -1
  97. package/dist/cst/reflection-attach.d.ts +8 -0
  98. package/dist/cst/reflection-attach.d.ts.map +1 -0
  99. package/dist/cst/reflection-symbols.d.ts +10 -0
  100. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  101. package/dist/cst/reflection.d.ts +2 -4
  102. package/dist/cst/reflection.d.ts.map +1 -1
  103. package/dist/cst/root-trivia-scope.d.ts +26 -0
  104. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  105. package/dist/cst/trivia-charscan.d.ts +18 -0
  106. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  107. package/dist/cst/trivia-kinds.d.ts +19 -0
  108. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  109. package/dist/functional/doc.d.ts.map +1 -1
  110. package/dist/functional/run-tabled.d.ts +12 -0
  111. package/dist/functional/run-tabled.d.ts.map +1 -0
  112. package/dist/functional/run.d.ts +25 -42
  113. package/dist/functional/run.d.ts.map +1 -1
  114. package/dist/grammar-metadata.d.ts +3 -0
  115. package/dist/grammar-metadata.d.ts.map +1 -0
  116. package/dist/index.cjs +10499 -7478
  117. package/dist/index.cjs.map +3 -4
  118. package/dist/index.d.ts +24 -9
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +10499 -7478
  121. package/dist/index.js.map +3 -4
  122. package/dist/language-service/index.cjs +311 -121
  123. package/dist/language-service/index.cjs.map +3 -4
  124. package/dist/language-service/index.js +311 -121
  125. package/dist/language-service/index.js.map +3 -4
  126. package/dist/oracle/index.cjs.map +0 -1
  127. package/dist/oracle/index.js.map +0 -1
  128. package/dist/parse-context.d.ts +49 -0
  129. package/dist/parse-context.d.ts.map +1 -0
  130. package/dist/plugin/evaluator.d.ts +41 -2
  131. package/dist/plugin/evaluator.d.ts.map +1 -1
  132. package/dist/plugin/free-identifiers.d.ts +23 -0
  133. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  134. package/dist/plugin/index.cjs +11381 -9442
  135. package/dist/plugin/index.cjs.map +3 -4
  136. package/dist/plugin/index.d.ts.map +1 -1
  137. package/dist/plugin/index.js +11381 -9442
  138. package/dist/plugin/index.js.map +3 -4
  139. package/dist/plugin/reducer-resolver.d.ts +17 -1
  140. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  141. package/dist/regex/classes.d.ts +28 -0
  142. package/dist/regex/classes.d.ts.map +1 -1
  143. package/dist/regex/first-set.d.ts +14 -0
  144. package/dist/regex/first-set.d.ts.map +1 -1
  145. package/dist/run/index.cjs +136 -51
  146. package/dist/run/index.cjs.map +2 -3
  147. package/dist/run/index.js +136 -51
  148. package/dist/run/index.js.map +2 -3
  149. package/dist/spec/index.cjs +2898 -1622
  150. package/dist/spec/index.cjs.map +3 -4
  151. package/dist/spec/index.js +2898 -1622
  152. package/dist/spec/index.js.map +3 -4
  153. package/dist/spec/model.d.ts +8 -0
  154. package/dist/spec/model.d.ts.map +1 -1
  155. package/dist/table/assemble.d.ts +307 -0
  156. package/dist/table/assemble.d.ts.map +1 -0
  157. package/dist/table/cell.d.ts +42 -0
  158. package/dist/table/cell.d.ts.map +1 -0
  159. package/dist/table/child-slots.d.ts +30 -0
  160. package/dist/table/child-slots.d.ts.map +1 -0
  161. package/dist/table/compile-rule-map.d.ts +136 -0
  162. package/dist/table/compile-rule-map.d.ts.map +1 -0
  163. package/dist/table/compile.d.ts +56 -0
  164. package/dist/table/compile.d.ts.map +1 -0
  165. package/dist/table/duplication-hook.d.ts +32 -0
  166. package/dist/table/duplication-hook.d.ts.map +1 -0
  167. package/dist/table/emit-assembly.d.ts +139 -0
  168. package/dist/table/emit-assembly.d.ts.map +1 -0
  169. package/dist/table/emit.d.ts +113 -0
  170. package/dist/table/emit.d.ts.map +1 -0
  171. package/dist/table/encode-baseline.d.ts +16 -0
  172. package/dist/table/encode-baseline.d.ts.map +1 -0
  173. package/dist/table/encode.d.ts +71 -0
  174. package/dist/table/encode.d.ts.map +1 -0
  175. package/dist/table/exec-baseline.d.ts +9 -0
  176. package/dist/table/exec-baseline.d.ts.map +1 -0
  177. package/dist/table/exec.d.ts +72 -0
  178. package/dist/table/exec.d.ts.map +1 -0
  179. package/dist/table/fold.d.ts +25 -0
  180. package/dist/table/fold.d.ts.map +1 -0
  181. package/dist/table/index.cjs +11375 -0
  182. package/dist/table/index.cjs.map +6 -0
  183. package/dist/table/index.d.ts +100 -0
  184. package/dist/table/index.d.ts.map +1 -0
  185. package/dist/table/index.js +11352 -0
  186. package/dist/table/index.js.map +6 -0
  187. package/dist/table/inspect.d.ts +17 -0
  188. package/dist/table/inspect.d.ts.map +1 -0
  189. package/dist/table/ops.d.ts +446 -0
  190. package/dist/table/ops.d.ts.map +1 -0
  191. package/dist/table/program.d.ts +493 -0
  192. package/dist/table/program.d.ts.map +1 -0
  193. package/dist/table/run-support.d.ts +38 -0
  194. package/dist/table/run-support.d.ts.map +1 -0
  195. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  196. package/dist/table/scan-shapes.d.ts.map +1 -0
  197. package/dist/table/site-labels.d.ts +58 -0
  198. package/dist/table/site-labels.d.ts.map +1 -0
  199. package/dist/table/stamp.d.ts +70 -0
  200. package/dist/table/stamp.d.ts.map +1 -0
  201. package/dist/types.d.ts +80 -24
  202. package/dist/types.d.ts.map +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/package.json +41 -3
  205. package/src/analysis/choice-cost-gate.ts +359 -0
  206. package/src/analysis/choice-cost-render.ts +293 -0
  207. package/src/analysis/choice-cost.ts +1008 -0
  208. package/src/analysis/commitment.ts +557 -0
  209. package/src/analysis/corpus.ts +130 -0
  210. package/src/analysis/diagnose-render.ts +625 -0
  211. package/src/analysis/diagnose.ts +317 -0
  212. package/src/analysis/diagnostics.ts +33 -0
  213. package/src/analysis/duplication.ts +1955 -0
  214. package/src/analysis/fix-render.ts +217 -0
  215. package/src/analysis/fix.ts +669 -0
  216. package/src/analysis/format-number.ts +31 -0
  217. package/src/analysis/gating.ts +792 -0
  218. package/src/analysis/grammar.ts +72 -0
  219. package/src/analysis/rebuild.ts +287 -0
  220. package/src/analysis/terminal.ts +336 -0
  221. package/src/cli/index.ts +413 -0
  222. package/src/combinators/adjacency.ts +171 -0
  223. package/src/combinators/attempt.ts +41 -0
  224. package/src/combinators/case-fold.ts +90 -0
  225. package/src/combinators/choice.ts +466 -0
  226. package/src/combinators/completions.ts +64 -0
  227. package/src/combinators/dispatch.ts +358 -0
  228. package/src/combinators/expect.ts +159 -0
  229. package/src/combinators/first-set.ts +322 -0
  230. package/src/combinators/gate.ts +50 -0
  231. package/src/combinators/grammar.ts +272 -0
  232. package/src/combinators/keywords.ts +145 -0
  233. package/src/combinators/literal.ts +102 -0
  234. package/src/combinators/map.ts +106 -0
  235. package/src/combinators/node.ts +345 -0
  236. package/src/combinators/not.ts +55 -0
  237. package/src/combinators/parser.ts +266 -0
  238. package/src/combinators/peek.ts +63 -0
  239. package/src/combinators/probe.ts +25 -0
  240. package/src/combinators/ref.ts +109 -0
  241. package/src/combinators/regex.ts +170 -0
  242. package/src/combinators/repeat.ts +561 -0
  243. package/src/combinators/scanTo.ts +393 -0
  244. package/src/combinators/sequence.ts +221 -0
  245. package/src/combinators/token.ts +120 -0
  246. package/src/combinators/trivia-boundary.ts +48 -0
  247. package/src/combinators/trivia-skip.ts +821 -0
  248. package/src/combinators/withCtx.ts +45 -0
  249. package/src/compiler/build-arity.ts +330 -0
  250. package/src/compiler/compile-linkable-table.ts +209 -0
  251. package/src/compiler/degradation.ts +266 -0
  252. package/src/compiler/fields.ts +191 -0
  253. package/src/compiler/grammar-coverage-ids.ts +129 -0
  254. package/src/compiler/ir-serialize.ts +615 -0
  255. package/src/compiler/linker.ts +954 -0
  256. package/src/compiler/token-alphabet.ts +209 -0
  257. package/src/compiler/token-dispatch.ts +353 -0
  258. package/src/compiler/token-scanner.ts +318 -0
  259. package/src/compiler/value-usage.ts +147 -0
  260. package/src/coverage.ts +383 -0
  261. package/src/cst/capture-buffer.ts +297 -0
  262. package/src/cst/host-mode.ts +69 -0
  263. package/src/cst/reflection-attach.ts +11 -0
  264. package/src/cst/reflection-symbols.ts +9 -0
  265. package/src/cst/reflection.ts +149 -0
  266. package/src/cst/relative-spans.ts +207 -0
  267. package/src/cst/root-trivia-scope.ts +30 -0
  268. package/src/cst/trivia-charscan.ts +312 -0
  269. package/src/cst/trivia-entries.ts +285 -0
  270. package/src/cst/trivia-index.ts +131 -0
  271. package/src/cst/trivia-kinds.ts +358 -0
  272. package/src/cst/types.ts +76 -0
  273. package/src/cst/walk.ts +101 -0
  274. package/src/functional/doc.ts +959 -0
  275. package/src/functional/run-tabled.ts +153 -0
  276. package/src/functional/run.ts +558 -0
  277. package/src/grammar-metadata.ts +2 -0
  278. package/src/index.ts +189 -0
  279. package/src/language-service/index.ts +221 -0
  280. package/src/line-index.ts +131 -0
  281. package/src/oracle/digest.ts +620 -0
  282. package/src/oracle/index.ts +55 -0
  283. package/src/parse-context.ts +91 -0
  284. package/src/plugin/direct-builder-static.ts +102 -0
  285. package/src/plugin/evaluator.ts +1505 -0
  286. package/src/plugin/free-identifiers.ts +338 -0
  287. package/src/plugin/index.ts +2281 -0
  288. package/src/plugin/reducer-resolver.ts +606 -0
  289. package/src/recovery/scan.ts +155 -0
  290. package/src/regex/classes.ts +183 -0
  291. package/src/regex/first-set.ts +316 -0
  292. package/src/run/index.ts +31 -0
  293. package/src/spec/ebnf.ts +128 -0
  294. package/src/spec/index.ts +54 -0
  295. package/src/spec/model.ts +636 -0
  296. package/src/spec/railroad-lib.ts +559 -0
  297. package/src/spec/railroad.ts +256 -0
  298. package/src/table/assemble.ts +2994 -0
  299. package/src/table/cell.ts +42 -0
  300. package/src/table/child-slots.ts +113 -0
  301. package/src/table/compile-rule-map.ts +336 -0
  302. package/src/table/compile.ts +244 -0
  303. package/src/table/duplication-hook.ts +89 -0
  304. package/src/table/emit-assembly.ts +1766 -0
  305. package/src/table/emit.ts +418 -0
  306. package/src/table/encode-baseline.ts +340 -0
  307. package/src/table/encode.ts +1569 -0
  308. package/src/table/exec-baseline.ts +537 -0
  309. package/src/table/exec.ts +1672 -0
  310. package/src/table/fold.ts +38 -0
  311. package/src/table/index.ts +108 -0
  312. package/src/table/inspect.ts +52 -0
  313. package/src/table/ops.ts +459 -0
  314. package/src/table/program.ts +855 -0
  315. package/src/table/run-support.ts +66 -0
  316. package/src/table/scan-shapes.ts +1578 -0
  317. package/src/table/site-labels.ts +228 -0
  318. package/src/table/stamp.ts +229 -0
  319. package/src/types.ts +563 -0
  320. package/src/version.ts +16 -0
  321. package/dist/compiler/codegen.d.ts +0 -334
  322. package/dist/compiler/codegen.d.ts.map +0 -1
  323. package/dist/compiler/inline-build.d.ts +0 -11
  324. package/dist/compiler/inline-build.d.ts.map +0 -1
  325. package/dist/compiler/inline-callback.d.ts +0 -21
  326. package/dist/compiler/inline-callback.d.ts.map +0 -1
  327. package/dist/compiler/line-index.d.ts +0 -2
  328. package/dist/compiler/line-index.d.ts.map +0 -1
  329. package/dist/compiler/scannable-run.d.ts.map +0 -1
  330. package/dist/compiler/scannable-terminal.d.ts +0 -23
  331. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  332. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  333. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,561 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+ import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTrivia } from './trivia-skip.ts'
3
+ import { matchesEmpty, startsFirstSet } from './first-set.ts'
4
+ import { deriveExpected } from './expect.ts'
5
+ import { matchesAt, orSentinel, recoverScan, captureError } from '../recovery/scan.ts'
6
+ import { demoteCapturedToRaw } from '../cst/capture-buffer.ts'
7
+
8
+ /**
9
+ * Parse one repetition item at `cur`, first skipping (and, in capture mode,
10
+ * recording) any leading trivia — so a repeating combinator consumes the trivia
11
+ * between items uniformly, the way advancing the index always should. Trivia is
12
+ * committed *before* the item so rawChildren order stays [item, trivia, item];
13
+ * if the item then fails or makes no progress the trivia is rolled back and the
14
+ * loop stops (the trivia is trailing and belongs to the enclosing context).
15
+ *
16
+ * Returns the item value + end position, the underlying failure (so oneOrMore
17
+ * can propagate a first-item failure), or 'stop'.
18
+ *
19
+ * `mandatory` marks an item `min` REQUIRES rather than one the greedy loop is
20
+ * merely trying. Both stops below — the EOF early-out and the zero-width stop —
21
+ * are TERMINATION DEVICES for an unbounded loop whose only source of progress is
22
+ * the item; that pressure does not exist for a prefix of exactly `min` items,
23
+ * which is finite by construction. A required item is therefore attempted at its
24
+ * position whatever is there, exactly as the first one already is (:203) — the
25
+ * trivia skip, which is real structure and not a stop, still applies to both.
26
+ */
27
+ function repItem<T>(
28
+ combinator: Combinator<T>,
29
+ input: string,
30
+ cur: number,
31
+ ctx: ParseContext,
32
+ guardable: boolean,
33
+ mandatory: boolean,
34
+ ): { value: T; end: number } | { fail: ParseResult<T>; failPos: number } | 'stop' {
35
+ const mark = saveTriviaMark(ctx)
36
+ let pos = cur
37
+ if (ctx.trivia) {
38
+ if (needsDeferredTriviaCommit(ctx)) {
39
+ const scan = scanTrivia(input, cur, ctx)
40
+ scan.commit()
41
+ pos = scan.end
42
+ } else {
43
+ pos = advanceTrivia(input, cur, ctx)
44
+ }
45
+ }
46
+ // Nothing but trivia left: don't speculatively parse an item at EOF (it would
47
+ // fail and could trigger an item's expect()/error side-effects). The trivia
48
+ // is trailing — roll it back for the enclosing context and stop. SPECULATIVE
49
+ // is the operative word: a `mandatory` item is not speculative, so it is
50
+ // attempted at EOF like the first item is, and a NULLABLE one matches there.
51
+ if (pos >= input.length && !mandatory) {
52
+ rollbackTrivia(ctx, mark)
53
+ return 'stop'
54
+ }
55
+ // First-set fast-path (mirrors emitMany's codegen guard): a body that can't match
56
+ // empty and whose first set can't start at `pos` can ONLY fail this iteration, so
57
+ // stop before the (composite) body's setup-then-fail. This is behaviour-identical
58
+ // in strict mode — a swallowed body failure is discarded either way, and a
59
+ // zero-width/leaf miss reaches the same loop stop. Skipped under a completions
60
+ // probe or tolerant recovery, where a swallowed failure still feeds the probe /
61
+ // triggers resync (matching the codegen guard's `!ctx.recovery` gate).
62
+ if (guardable && ctx._probe === undefined && !ctx._tolerant && !startsFirstSet(combinator, input, pos)) {
63
+ rollbackTrivia(ctx, mark)
64
+ return 'stop'
65
+ }
66
+ const result = combinator.parse(input, pos, ctx)
67
+ if (!result.ok) {
68
+ rollbackTrivia(ctx, mark)
69
+ if (result.committed) return { fail: result, failPos: pos }
70
+ // Surface the POST-trivia position where the element actually failed. The
71
+ // tolerant recovery guard must check the sync token there — not at `cur`,
72
+ // which sits before any leading trivia — so trailing trivia before the sync
73
+ // isn't mistaken for junk and swallowed into a spurious ParseError.
74
+ return { fail: result, failPos: pos }
75
+ }
76
+ // Zero-width item: it cannot make progress, so the greedy loop stops without
77
+ // taking it — a TERMINATION device, not a semantic filter (the same rule the
78
+ // table driver holds to `viaRepItem`). A `mandatory` item is counted whatever
79
+ // its width: `min` counts ITEMS, and `x*` over a nullable `x` derives the empty
80
+ // string with any number of them, so a `{ min: n }` prefix takes the n-item
81
+ // derivation. It cannot spin — the prefix is exactly `n` long.
82
+ if (result.span.end === pos && !mandatory) {
83
+ rollbackTrivia(ctx, mark)
84
+ return 'stop'
85
+ }
86
+ return { value: result.value, end: result.span.end }
87
+ }
88
+
89
+ export type RepeatOptions = {
90
+ /**
91
+ * Minimum number of ITEMS. Default `0`.
92
+ *
93
+ * `min >= 1` is not a validation nicety — it is what makes the combinator
94
+ * NON-NULLABLE. A nullable arm matches at every position, which disables its
95
+ * `choice`'s first-char dispatch by parseman's own first-set rule; a `min >= 1`
96
+ * repeat keeps the item's first-set, so an arm led by it still gates.
97
+ */
98
+ min?: number
99
+ /** Maximum number of ITEMS. Default: unbounded. Never affects nullability. */
100
+ max?: number
101
+ }
102
+
103
+ /** Shared `min`/`max` validation — a bad bound is an authoring error, not a parse
104
+ * outcome, so it throws at CONSTRUCTION where the stack points at the grammar. */
105
+ function resolveBounds(what: string, opts: { min?: number; max?: number }): { min: number; max: number } {
106
+ const min = opts.min ?? 0
107
+ const max = opts.max ?? Infinity
108
+ const bad = (msg: string): never => { throw new RangeError(`parseman: ${what} ${msg}`) }
109
+ if (!Number.isInteger(min) || min < 0) bad(`min must be a non-negative integer (got ${String(opts.min)})`)
110
+ if (max !== Infinity && (!Number.isInteger(max) || max < 1)) bad(`max must be a positive integer (got ${String(opts.max)})`)
111
+ if (max < min) bad(`max (${max}) is less than min (${min}) — the combinator could never succeed`)
112
+ return { min, max }
113
+ }
114
+
115
+ /**
116
+ * Repetition: `item*` by default, bounded by `{ min, max }` (both count ITEMS).
117
+ *
118
+ * many(g.Decl) // zero or more — NULLABLE
119
+ * many(g.Decl, { min: 1 }) // one or more — same as oneOrMore(g.Decl)
120
+ * many(g.HexDigit, { min: 3, max: 8 })
121
+ *
122
+ * `oneOrMore(x)` is kept as the sugar for the common `{ min: 1 }` case.
123
+ */
124
+ export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Combinator<T[]> {
125
+ const { min, max } = resolveBounds('many()', opts)
126
+ // `min >= 1` routes to the SAME implementation `oneOrMore` has always used, so
127
+ // `many(x, { min: 1 })` and `oneOrMore(x)` are the identical combinator — not
128
+ // merely equivalent-looking. (They differ from the min-0 loop in one real way:
129
+ // the mandatory items parse at `pos` with no leading-trivia skip, which is the
130
+ // enclosing context's job.)
131
+ if (min >= 1) return atLeast(combinator, min, max)
132
+ const meta: ParserMeta = {
133
+ firstSet: combinator._meta.firstSet,
134
+ canMatchNewline: combinator._meta.canMatchNewline,
135
+ isTrivia: false,
136
+ }
137
+ const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean } =
138
+ { tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }) }
139
+ let expected: string[] | undefined
140
+ // A non-nullable body can be first-set-gated per loop iteration (see repItem).
141
+ const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
142
+
143
+ return {
144
+ _tag: 'many',
145
+ _meta: meta,
146
+ _def: def,
147
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
148
+ // When the aggregate is never observed (markUnusedValues), skip the array:
149
+ // items still parse and self-capture into the enclosing node's children.
150
+ const values: T[] | undefined = def.valueUnused ? undefined : []
151
+ let cur = pos
152
+ let count = 0
153
+ while (cur < input.length) {
154
+ if (count >= max) break
155
+ const item = repItem(combinator, input, cur, ctx, guardable, false)
156
+ if (item === 'stop') break
157
+ if ('fail' in item) {
158
+ if (!item.fail.ok && item.fail.committed) return item.fail
159
+ // Cold path: only reached on an element failure. Strict mode ⇒ `break`.
160
+ // Tolerant ⇒ resync to the sync sentinel the enclosing sequence inferred
161
+ // and published as ctx._sync (the grammar carries no recovery config). No
162
+ // sync available ⇒ nothing to skip to → break.
163
+ const sync = ctx._tolerant ? ctx._sync : undefined
164
+ if (sync === undefined) break
165
+ // Sync token at the POST-trivia failure position ⇒ clean list end (the
166
+ // trailing trivia belongs to the enclosing context), not junk. Checking
167
+ // `item.failPos` (past leading trivia), not `cur`, keeps trivia out of
168
+ // both the break decision and the recovered error span.
169
+ if (matchesAt(sync, input, item.failPos, ctx)) break
170
+ expected ??= deriveExpected(combinator)
171
+ const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected)
172
+ if (values !== undefined) values.push(error as unknown as T)
173
+ captureError(ctx, error)
174
+ count++
175
+ cur = end
176
+ continue
177
+ }
178
+ if (values !== undefined) values.push(item.value)
179
+ count++
180
+ cur = item.end
181
+ }
182
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
183
+ return { ok: true, value: (values ?? undefined) as T[], span: { start: pos, end: cur } }
184
+ },
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Sugar for the commonest bound: `many(combinator, { min: 1 })` — the identical
190
+ * combinator, not merely an equivalent one.
191
+ */
192
+ export function oneOrMore<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Combinator<T[]> {
193
+ return many(combinator, { ...opts, min: opts.min ?? 1 })
194
+ }
195
+
196
+ /**
197
+ * `min >= 1` repetition: `min` MANDATORY items (whose failure propagates) then a
198
+ * greedy loop up to `max`. The tag stays `oneOrMore` — every downstream switch
199
+ * keys on it for "non-nullable repeat" — and `min` carries the real count.
200
+ */
201
+ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combinator<T[]> {
202
+ const meta: ParserMeta = {
203
+ firstSet: combinator._meta.firstSet,
204
+ canMatchNewline: combinator._meta.canMatchNewline,
205
+ isTrivia: false,
206
+ }
207
+ const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean } =
208
+ { tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }) }
209
+ let expected: string[] | undefined
210
+ // A non-nullable body can be first-set-gated per loop iteration (see repItem).
211
+ const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
212
+
213
+ return {
214
+ _tag: 'oneOrMore',
215
+ _meta: meta,
216
+ _def: def,
217
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
218
+ // First item is mandatory (parsed at pos directly — leading trivia is the
219
+ // enclosing context's responsibility); subsequent items skip leading trivia.
220
+ const first = combinator.parse(input, pos, ctx)
221
+ if (!first.ok) return first
222
+ // Aggregate skipped when never observed (see `many`).
223
+ const values: T[] | undefined = def.valueUnused ? undefined : [first.value]
224
+ let cur = first.span.end
225
+ let count = 1
226
+ // Mandatory items 2..min (only entered for min > 1) — each failure propagates,
227
+ // exactly like the first. They go through repItem so the trivia BETWEEN items
228
+ // is consumed the same way the loop consumes it, but `mandatory` holds off its
229
+ // two loop-termination stops: a required item is attempted at its position
230
+ // whatever is there, and counts whatever its width.
231
+ while (count < min) {
232
+ const item = repItem(combinator, input, cur, ctx, guardable, true)
233
+ if (item === 'stop' || 'fail' in item) {
234
+ if (item !== 'stop' && !item.fail.ok && item.fail.committed) return item.fail
235
+ // Anchored at `cur` — the furthest position the repeat reached — not at
236
+ // its start. `many(regex(/x/), { min: 3 })` over "xx" consumed both x's
237
+ // and got stuck at 2 wanting a third; reporting offset 0 points the
238
+ // caret at input that matched. Same rule as `sepBy`'s `failAt`, and the
239
+ // compiled emitter already anchored here — this side was the outlier.
240
+ expected ??= deriveExpected(combinator)
241
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } }
242
+ }
243
+ if (values !== undefined) values.push(item.value)
244
+ cur = item.end
245
+ count++
246
+ }
247
+ while (cur < input.length) {
248
+ if (count >= max) break
249
+ const item = repItem(combinator, input, cur, ctx, guardable, false)
250
+ if (item === 'stop') break
251
+ if ('fail' in item) {
252
+ if (!item.fail.ok && item.fail.committed) return item.fail
253
+ // Cold path (element failure). Strict: break. Tolerant: resync — see many().
254
+ const sync = ctx._tolerant ? ctx._sync : undefined
255
+ if (sync === undefined) break
256
+ if (matchesAt(sync, input, item.failPos, ctx)) break
257
+ expected ??= deriveExpected(combinator)
258
+ const { error, end } = recoverScan(input, item.failPos, ctx, sync, expected)
259
+ if (values !== undefined) values.push(error as unknown as T)
260
+ captureError(ctx, error)
261
+ count++
262
+ cur = end
263
+ continue
264
+ }
265
+ if (values !== undefined) values.push(item.value)
266
+ count++
267
+ cur = item.end
268
+ }
269
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
270
+ return { ok: true, value: (values ?? undefined) as T[], span: { start: pos, end: cur } }
271
+ },
272
+ }
273
+ }
274
+
275
+ export function optional<T>(combinator: Combinator<T>): Combinator<T | null> {
276
+ const meta: ParserMeta = {
277
+ firstSet: combinator._meta.firstSet,
278
+ canMatchNewline: combinator._meta.canMatchNewline,
279
+ isTrivia: false,
280
+ }
281
+ const firstSetSkippable = !matchesEmpty(combinator)
282
+
283
+ return {
284
+ _tag: 'optional',
285
+ _meta: meta,
286
+ _def: { tag: 'optional', parser: combinator as Combinator<unknown> },
287
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T | null> {
288
+ if (firstSetSkippable && ctx._probe === undefined && !startsFirstSet(combinator, input, pos)) {
289
+ return { ok: true, value: null, span: { start: pos, end: pos } }
290
+ }
291
+ const mark = saveTriviaMark(ctx)
292
+ const result = combinator.parse(input, pos, ctx)
293
+ if (result.ok) return result as ParseResult<T>
294
+ // Inner failed → roll back any CST leaves/trivia it captured before giving up.
295
+ rollbackTrivia(ctx, mark)
296
+ if (result.committed) return result
297
+ return { ok: true, value: null, span: { start: pos, end: pos } }
298
+ },
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Non-empty separated list — `oneOrMore`'s relationship to `many`, for separated
304
+ * lists. `oneOrMoreSep(item, sep)` is exactly `sepBy(item, sep, { min: 1 })`.
305
+ *
306
+ * oneOrMoreSep(g.Selector, literal(',')) // a selector list is never empty
307
+ *
308
+ * REACH FOR THIS, NOT `sepBy`, for any list that cannot actually be empty —
309
+ * selector lists, value lists, media-query preludes, keyframe selectors. `sepBy`'s
310
+ * min-0 default matches the EMPTY STRING, which makes it nullable, and a nullable
311
+ * arm disables its `choice`'s first-char dispatch by parseman's own first-set
312
+ * rule. This form is non-nullable and keeps the item's first-set, so an arm led by
313
+ * it still gates.
314
+ */
315
+ export function oneOrMoreSep<T, S>(combinator: Combinator<T>, separator: Combinator<S> | KeptSeparator<S>, opts: SepByOptions = {}): Combinator<T[]> {
316
+ return sepBy(combinator, separator, { ...opts, min: opts.min ?? 1 })
317
+ }
318
+
319
+ /** How a separator with NO item after it is treated. */
320
+ export type TrailingSeparator =
321
+ /**
322
+ * DEFAULT, and what `sepBy` has always done: the trailing separator is NOT
323
+ * consumed — the list ends before it and the enclosing grammar sees it. (It is
324
+ * not an error here; "forbid" means the list refuses to own it.)
325
+ */
326
+ | 'forbid'
327
+ /** Consume a trailing separator when present (`a, b,` → 2 items, comma eaten). */
328
+ | 'allow'
329
+
330
+ export type SepByOptions = RepeatOptions & {
331
+ /** What to do with a separator that has no item after it. Default `'forbid'`. */
332
+ trailing?: TrailingSeparator
333
+ }
334
+
335
+ /** A separator the author asked to keep in `children`. Produced by `keepSeparator`. */
336
+ export type KeptSeparator<S> = { readonly _keepSeparator: Combinator<S> }
337
+
338
+ /**
339
+ * Keep this list's separators in `children`, interleaved with the items.
340
+ *
341
+ * A list contributes its ITEMS and nothing else — that is the default and it is
342
+ * not negotiable, because a `children` array whose arity depends on a detail of
343
+ * the separator is the thing that made this defect invisible. But a combinator
344
+ * may collapse only what its CONSTRUCTION makes recoverable, and that is exactly
345
+ * the line this helper draws:
346
+ *
347
+ * sepBy(g.Value, literal(',')) // ',' is fixed here — recoverable, drop it
348
+ * sepBy(g.Track, keepSeparator(SLASH_OR_COMMA)) // could be '/' OR ',' — NOT recoverable, keep it
349
+ *
350
+ * Wrap the separator when it could have matched more than one thing: a `choice`,
351
+ * a regex with alternation or a quantifier, a rule reference. In CSS the
352
+ * separator carries meaning — `grid-area: 1 / 2` and `font: 12px/1.5` do not mean
353
+ * what `1, 2` means — and dropping it destroys information that exists nowhere
354
+ * else in the tree.
355
+ *
356
+ * The wrap is read at CONSTRUCTION, not per parse, and it is deliberately applied
357
+ * to the separator rather than passed as an option: the call site then STATES its
358
+ * own children arity, which is the failure being fixed. `keepSeparator` in the
359
+ * source is the only documentation that reaches an author who never reads docs.
360
+ */
361
+ export function keepSeparator<S>(separator: Combinator<S>): KeptSeparator<S> {
362
+ return { _keepSeparator: separator }
363
+ }
364
+
365
+ function unwrapSeparator<S>(separator: Combinator<S> | KeptSeparator<S>): { sep: Combinator<S>; keep: boolean } {
366
+ return '_keepSeparator' in separator
367
+ ? { sep: separator._keepSeparator, keep: true }
368
+ : { sep: separator, keep: false }
369
+ }
370
+
371
+ /**
372
+ * Separated list: `(item (sep item)*)?` by default — note that it MATCHES THE
373
+ * EMPTY STRING, which makes it nullable and therefore un-gateable as a choice arm.
374
+ * For a list that cannot be empty reach for `oneOrMoreSep`, or pass `{ min: 1 }`.
375
+ *
376
+ * sepBy(g.Value, literal(',')) // may be empty — NULLABLE
377
+ * oneOrMoreSep(g.Selector, literal(',')) // non-empty — gates as a choice arm
378
+ * sepBy(g.Decl, literal(';'), { trailing: 'allow' })
379
+ *
380
+ * `min`/`max` count ITEMS, not separators.
381
+ */
382
+ export function sepBy<T, S>(
383
+ combinator: Combinator<T>,
384
+ separatorArg: Combinator<S> | KeptSeparator<S>,
385
+ opts: SepByOptions = {},
386
+ ): Combinator<T[]> {
387
+ const { sep: separator, keep: keepSeparators } = unwrapSeparator(separatorArg)
388
+ const { min, max } = resolveBounds('sepBy()', opts)
389
+ const trailing = opts.trailing ?? 'forbid'
390
+ const meta: ParserMeta = {
391
+ firstSet: combinator._meta.firstSet,
392
+ canMatchNewline: combinator._meta.canMatchNewline || separator._meta.canMatchNewline,
393
+ isTrivia: false,
394
+ }
395
+ let expected: string[] | undefined
396
+ /**
397
+ * A list-level failure is anchored at the FURTHEST position the list reached,
398
+ * and reports what would have allowed it to CONTINUE there — never at the
399
+ * list's own start.
400
+ *
401
+ * Anchoring at the start throws away everything the parse learned: for
402
+ * `sepBy(item, ',', { min: 3 })` over `"a,b"` the list consumed `a,b` perfectly
403
+ * and got stuck at offset 3 wanting a third item, so a failure reported at 0
404
+ * points the caret at a token that parsed fine. It also mislocates
405
+ * `completionsAt`, which asks what may appear AT a cursor — the answer belongs
406
+ * at the stuck position, not at the list's start.
407
+ *
408
+ * BOTH engines must follow this rule; it drifted three separate ways across
409
+ * this option surface, and `test/parity/repeat-options-parity.test.ts` is what
410
+ * now holds them together. The empty-set fallback mirrors codegen's
411
+ * `deriveExpectedArr`, so the two payloads are identical.
412
+ */
413
+ const failAt = (at: number): ParseResult<T[]> => {
414
+ expected ??= deriveExpected(combinator)
415
+ return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } }
416
+ }
417
+
418
+ return {
419
+ _tag: 'sepBy',
420
+ _meta: meta,
421
+ _def: {
422
+ tag: 'sepBy', parser: combinator as Combinator<unknown>, separator: separator as Combinator<unknown>, min,
423
+ ...(max === Infinity ? {} : { max }),
424
+ ...(trailing === 'forbid' ? {} : { trailing }),
425
+ // Carried into the IR so the COMPILED engine demotes exactly where the
426
+ // interpreter does. The two engines have drifted three separate ways across
427
+ // this option surface already; parity is held by test/parity, not by hope.
428
+ ...(keepSeparators ? { keepSeparators: true } : {}),
429
+ },
430
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
431
+ const first = combinator.parse(input, pos, ctx)
432
+ const values: T[] = []
433
+ let cur: number
434
+ if (first.ok) {
435
+ values.push(first.value)
436
+ cur = first.span.end
437
+ } else {
438
+ if (first.committed) return first
439
+ // Cold path. Strict: an empty/absent first element is a legal empty list
440
+ // for `sepBy`, and a FAILURE for `sepBy1` (min 1 — that is the whole point).
441
+ // Tolerant: if the first element is JUNK (a terminator is inferable and we
442
+ // are not already sitting on it) recover it and enter the loop; otherwise
443
+ // it is a genuine empty list.
444
+ const term = ctx._tolerant ? ctx._sync : undefined
445
+ if (term === undefined || matchesAt(term, input, pos, ctx)) {
446
+ // `min >= 1` — nothing was consumed, so the furthest position IS `pos`.
447
+ // The compiled form swallows the first element's sub-parse (it is
448
+ // discarded on the min-0 path), so it cannot reproduce the inner
449
+ // failure's exact payload; both sides report this same derived set.
450
+ if (min >= 1) return failAt(pos)
451
+ return { ok: true, value: [], span: { start: pos, end: pos } }
452
+ }
453
+ expected ??= deriveExpected(combinator)
454
+ const rec = recoverScan(input, pos, ctx, orSentinel(separator, term), expected)
455
+ values.push(rec.error as unknown as T)
456
+ captureError(ctx, rec.error)
457
+ cur = rec.end
458
+ }
459
+ // `|| values.length < min` — the separated twin of `repItem`'s `mandatory`
460
+ // (1c5b2e8). `cur < input.length` is a TERMINATION device for the greedy
461
+ // tail, and that pressure does not exist for a prefix of exactly `min`
462
+ // items, which is finite by construction: each iteration that succeeds
463
+ // pushes one, so the disjunct can hold at most `min` times. Stopping on it
464
+ // made a REQUIRED item unreachable at EOF, so `sepBy(x, s, { min: n })` over
465
+ // a nullable `x` AND a nullable `s` — where the n-item derivation of the
466
+ // empty string genuinely exists — failed instead of taking it.
467
+ //
468
+ // A non-nullable separator still fails the list here rather than padding it:
469
+ // n items need n-1 separators, so there is no derivation to take, and the
470
+ // iteration below breaks on the separator exactly as it always did.
471
+ // Vacuous for `min <= 1`, which the first element already satisfies.
472
+ while (cur < input.length || values.length < min) {
473
+ if (values.length >= max) break
474
+ // One mark for the whole iteration (separator + following item): if the
475
+ // item fails, the trailing separator must be rolled back with it, or its
476
+ // captured leaves leak past the end of the list.
477
+ const loopMark = saveTriviaMark(ctx)
478
+ let sepPos = cur
479
+ if (ctx.trivia) {
480
+ if (needsDeferredTriviaCommit(ctx)) {
481
+ const scan = scanTrivia(input, cur, ctx)
482
+ scan.commit()
483
+ sepPos = scan.end
484
+ } else {
485
+ sepPos = advanceTrivia(input, cur, ctx)
486
+ }
487
+ }
488
+ const sep = separator.parse(input, sepPos, ctx)
489
+ if (!sep.ok) {
490
+ rollbackTrivia(ctx, loopMark)
491
+ if (sep.committed) return sep
492
+ break
493
+ }
494
+ // A LIST CONTRIBUTES ITS ITEMS AND NOTHING ELSE. The separator matched, so
495
+ // it is consumed and it is recorded in `rawChildren` — but it is not an
496
+ // item, so it does not belong in the structural `children` array. Demote it
497
+ // using the mark this iteration already took; no extra allocation, one
498
+ // guarded truncation, and only when CST capture is live at all.
499
+ if (!keepSeparators) demoteCapturedToRaw(ctx, loopMark.leaves)
500
+ // Mark taken AFTER the separator: `trailing` keeps the separator but must
501
+ // still unwind the trivia + leaves captured past it.
502
+ const sepMark = trailing === 'forbid' ? undefined : saveTriviaMark(ctx)
503
+ let nextPos = sep.span.end
504
+ if (ctx.trivia) {
505
+ if (needsDeferredTriviaCommit(ctx)) {
506
+ const scan = scanTrivia(input, sep.span.end, ctx)
507
+ scan.commit()
508
+ nextPos = scan.end
509
+ } else {
510
+ nextPos = advanceTrivia(input, sep.span.end, ctx)
511
+ }
512
+ }
513
+ const next = combinator.parse(input, nextPos, ctx)
514
+ if (!next.ok) {
515
+ if (next.committed) return next
516
+ // Cold path. Strict: roll back the trailing separator + break. Tolerant:
517
+ // the separator we just consumed is real, so resync the bad element after
518
+ // it. If a terminator is inferable and already present at nextPos, the
519
+ // separator was a trailing one (e.g. `a;}`) → roll it back and stop.
520
+ //
521
+ // RECOVERY IS TESTED FIRST, and is ORTHOGONAL to `trailing`. This block
522
+ // used to lead with the `sepMark !== undefined` branch — but sepMark is
523
+ // non-undefined for EVERY 'allow' list, so that branch always won and the
524
+ // resync below became unreachable the moment a grammar opted into a
525
+ // trailing separator. `{a,,b}` recovered under the default 'forbid' and
526
+ // hard-failed under 'allow', which is not a policy anyone would choose:
527
+ // permitting a trailing comma has nothing to do with whether a tolerant
528
+ // parse may resynchronize. (The compiled `failItem` already ordered it
529
+ // this way — the engines disagreed because of this.)
530
+ //
531
+ // Junk after a REAL separator is recovered whatever `trailing` says, and
532
+ // no rollback happens on that path: both the separator and the recovered
533
+ // error element belong to the list.
534
+ const term = ctx._tolerant ? ctx._sync : undefined
535
+ if (term !== undefined && !matchesAt(term, input, nextPos, ctx)) {
536
+ expected ??= deriveExpected(combinator)
537
+ const rec = recoverScan(input, nextPos, ctx, orSentinel(separator, term), expected)
538
+ values.push(rec.error as unknown as T)
539
+ captureError(ctx, rec.error)
540
+ cur = rec.end
541
+ continue
542
+ }
543
+ if (sepMark !== undefined) {
544
+ // 'allow': the separator we just consumed IS part of the list — a
545
+ // genuine trailing one, since no resync applied above.
546
+ rollbackTrivia(ctx, sepMark)
547
+ cur = sep.span.end
548
+ break
549
+ }
550
+ rollbackTrivia(ctx, loopMark)
551
+ break
552
+ }
553
+ values.push(next.value)
554
+ cur = next.span.end
555
+ }
556
+ // Too few items: the list is stuck at `cur` wanting another ITEM.
557
+ if (values.length < min) return failAt(cur)
558
+ return { ok: true, value: values, span: { start: pos, end: cur } }
559
+ },
560
+ }
561
+ }