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,393 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+ import { literal } from './literal.ts'
3
+ import { regex } from './regex.ts'
4
+ import { sequence } from './sequence.ts'
5
+ import { choice } from './choice.ts'
6
+ import { many } from './repeat.ts'
7
+ import { transform } from './map.ts'
8
+ import { expect } from './expect.ts'
9
+ import { createDetachedParseContext } from '../parse-context.ts'
10
+ import { any } from './first-set.ts'
11
+ import { ref } from './ref.ts'
12
+ import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
13
+ import { token } from './token.ts'
14
+ import { recordLineRangeFromContext } from '../line-index.ts'
15
+
16
+ export type ScanToOptions = {
17
+ /**
18
+ * Per-call opaque-unit skippers (balanced parens/brackets, dialect
19
+ * interpolation, …). These EXTEND the grammar-level ambient default: ambient
20
+ * trivia (comments/ws) and ambient `scanSkip` (strings) are applied too, unless
21
+ * `raw`. So a site only needs to list the extra units its scan requires.
22
+ */
23
+ skip?: Combinator<unknown>[]
24
+ /**
25
+ * Hard opt-out: skip NOTHING ambiently — no trivia, no scanSkip, and (with no
26
+ * per-call `skip`) a pure raw byte-walk. For the rare site that intends to scan
27
+ * literally through comments/strings.
28
+ */
29
+ raw?: boolean
30
+ /**
31
+ * If true, reaching EOF without finding the sentinel is a success — returns
32
+ * everything consumed so far. Default false (fail at EOF).
33
+ */
34
+ orEOF?: boolean
35
+ /**
36
+ * `balanced()` only. Make an unmatched close a genuine FAILURE instead of a
37
+ * recovered one.
38
+ *
39
+ * By default `balanced()` wraps its close in `expect()`, which never fails: on
40
+ * a miss it returns a ParseError, records it on `ctx._errors`, and reports a
41
+ * zero-width span. So the combinator is UNFAILABLE once its opener is consumed
42
+ * — the rejection is computed and recorded, but a caller cannot branch on it.
43
+ * `choice()` cannot fall through to another arm, `not()` cannot negate it, and
44
+ * a `sequence()` around it proceeds as if the group closed.
45
+ *
46
+ * With `strict: true` the close is required, so an unmatched or missing close
47
+ * fails the whole group and rolls back to the opener — the ordinary combinator
48
+ * contract. Nested groups inherit strictness (the interior recurses into the
49
+ * same combinator).
50
+ *
51
+ * Opt-in: the default is unchanged, because recovery is what a tolerant
52
+ * document parse wants, and existing grammars are built on it.
53
+ */
54
+ strict?: boolean
55
+ }
56
+
57
+ /**
58
+ * Resolve the effective ordered skipper list for a scan, folding grammar-level
59
+ * ambient trivia + scanSkip in FRONT of the per-call `skip` (explicit skip
60
+ * EXTENDS the ambient default). Shared by the interpreter `scanTo`/`balanced`;
61
+ * the compiled path bakes the identical list in codegen.
62
+ *
63
+ * raw → [] (no trivia, no scanSkip, no skip)
64
+ * else → [ ...trivia?, ...scanSkip?, ...skip ]
65
+ *
66
+ * The ambient trivia (comments/ws) leads so a sentinel hidden in a comment is
67
+ * never matched, then ambient strings, then the site's extra units. The sentinel
68
+ * itself is still checked before any skipper, so a sentinel that also starts a
69
+ * skip region wins (unchanged priority).
70
+ */
71
+ export function resolveScanSkip(
72
+ explicitSkip: Combinator<unknown>[],
73
+ raw: boolean,
74
+ ctx: ParseContext,
75
+ ): Combinator<unknown>[] {
76
+ if (raw) return []
77
+ const trivia = ctx.trivia
78
+ const ambient = ctx.scanSkip
79
+ if (!trivia && !ambient) return explicitSkip
80
+ const out: Combinator<unknown>[] = []
81
+ if (trivia) out.push(trivia)
82
+ if (ambient) out.push(...ambient)
83
+ out.push(...explicitSkip)
84
+ return out
85
+ }
86
+
87
+ /**
88
+ * Consume input up to (but not including) the sentinel, skipping over any
89
+ * "hole" patterns in order so their contents are never mistaken for the sentinel.
90
+ *
91
+ * Returns the consumed text as a string. The sentinel is NOT consumed.
92
+ * Fails if the sentinel is never found (unless orEOF is true).
93
+ *
94
+ * const selector = scanTo(literal('{'), {
95
+ * skip: [cssComment, stringLit, balanced('(', ')'), balanced('[', ']')],
96
+ * })
97
+ */
98
+ export function scanTo(
99
+ sentinel: Combinator<unknown>,
100
+ { skip, raw = false, orEOF = false }: ScanToOptions = {},
101
+ ): Combinator<string> {
102
+ const meta: ParserMeta = {
103
+ firstSet: any(),
104
+ canMatchNewline: true,
105
+ isTrivia: false,
106
+ }
107
+ const explicitSkip = skip ?? []
108
+
109
+ return {
110
+ _tag: 'scanTo',
111
+ _meta: meta,
112
+ _def: { tag: 'scanTo', sentinel, skip: explicitSkip, raw, orEOF },
113
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
114
+ let cur = pos
115
+ // Fold grammar-level ambient trivia + scanSkip into the effective skippers.
116
+ const skip = resolveScanSkip(explicitSkip, raw, ctx)
117
+
118
+ // Sentinel checks and skip scans must not emit CST children of their own —
119
+ // scanTo represents the whole scanned span as one leaf. Probe them with a
120
+ // collector-free context so their internal literal()/regex() don't push.
121
+ // The error channel IS forwarded, so a committed skipper (e.g. balanced()
122
+ // whose open delimiter was consumed) can still report an unmatched close.
123
+ const probeCtx = createDetachedParseContext(false, ctx.state)
124
+ probeCtx._errors = ctx._errors
125
+
126
+ // Record the scanned text as a CSTLeaf so buildNode-driven grammars can
127
+ // see it in children/rawChildren (it would otherwise be lost — only the
128
+ // returned value carries it). Skipped when no collector is active.
129
+ const emit = (end: number) => {
130
+ if (end > pos && cstCaptureActive(ctx)) {
131
+ const leaf = { _tag: 'leaf', value: input.slice(pos, end), span: { start: pos, end } }
132
+ pushCstLeaf(ctx, leaf)
133
+ }
134
+ }
135
+
136
+ while (cur < input.length) {
137
+ // Check sentinel — if it matches here, stop and return consumed text.
138
+ const s = sentinel.parse(input, cur, probeCtx)
139
+ if (s.ok) {
140
+ if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
141
+ emit(cur)
142
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
143
+ }
144
+
145
+ // Try each skipper in order; take first that advances.
146
+ let advanced = false
147
+ for (const skipper of skip) {
148
+ const r = skipper.parse(input, cur, probeCtx)
149
+ if (r.ok && r.span.end > cur) {
150
+ cur = r.span.end
151
+ advanced = true
152
+ break
153
+ }
154
+ }
155
+
156
+ // Nothing matched — consume one character and continue.
157
+ if (!advanced) cur++
158
+ }
159
+
160
+ // Reached EOF without finding sentinel.
161
+ if (orEOF) {
162
+ if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
163
+ emit(cur)
164
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
165
+ }
166
+ const sentDef = sentinel._def
167
+ const expected = sentDef.tag === 'literal' ? [JSON.stringify(sentDef.value)] : ['sentinel']
168
+ return { ok: false, expected, span: { start: pos, end: cur } }
169
+ },
170
+ }
171
+ }
172
+
173
+ /** Codegen marker: a balanced combinator that must re-resolve ambient scanSkip
174
+ * into its interior at emit time (the compiled mirror of the interpreter wrapper). */
175
+ export type BalancedAmbient = Combinator<string> & {
176
+ _balancedAmbient?: { open: string; close: string; ownSkip: Combinator<unknown>[]; strict?: boolean }
177
+ }
178
+
179
+ /**
180
+ * Marker: this combinator IS a balanced interior — its delimiters, and the fact
181
+ * that everything between them is a scan rather than authored structure.
182
+ *
183
+ * Set by `buildBalancedInterior`, so EVERY balanced carries it: the ambient one,
184
+ * the `raw` one, and each interior the ambient cache rebuilds. `_balancedAmbient`
185
+ * cannot serve this purpose — it is absent on `raw`, and it means "re-resolve
186
+ * ambient scanSkip", which is a different claim.
187
+ *
188
+ * Read by the spec/railroad emitter, which renders a balanced as its delimiters
189
+ * around an opaque interior. That is not a simplification: the delimiters are
190
+ * FIXED at construction and the interior genuinely is a delimiter scan, so the
191
+ * rendering states exactly what the construct is. Expanding the lowered shape
192
+ * instead would print the content-run regex and the `self` back-edge, which are
193
+ * emitter machinery, not language.
194
+ */
195
+ export type BalancedMarked = Combinator<string> & {
196
+ _balanced?: { open: string; close: string }
197
+ }
198
+
199
+ /**
200
+ * The table lowering's marker, on the object `balanced()` RETURNS.
201
+ *
202
+ * Deliberately not `_balancedAmbient`, and deliberately on the OUTER combinator.
203
+ * `_balancedAmbient` means "rebuild my interior with the ambient scanSkip", which
204
+ * is false for `raw`, and it sits on the INNER combinator because that is the one
205
+ * codegen's dedup and the interior `self` back-edge address. Reading `_def` (or
206
+ * the ambient marker) off the outer object gets the wrong thing — a structural
207
+ * encoder made exactly that mistake and lowered the wrong parser, silently.
208
+ *
209
+ * This marker instead records the CONSTRUCTOR ARGUMENTS, so an encoder can carry
210
+ * a `balanced()` as data and let `balanced()` itself rebuild it. It is present on
211
+ * every `balanced()`, `raw` included.
212
+ */
213
+ export type BalancedSpec = Combinator<string> & {
214
+ _balancedSpec?: {
215
+ open: string
216
+ close: string
217
+ ownSkip: Combinator<unknown>[]
218
+ strict: boolean
219
+ raw: boolean
220
+ }
221
+ }
222
+
223
+ function markSpec(
224
+ outer: Combinator<string>,
225
+ open: string, close: string, ownSkip: Combinator<unknown>[], strict: boolean, raw: boolean,
226
+ ): Combinator<string> {
227
+ ;(outer as BalancedSpec)._balancedSpec = { open, close, ownSkip, strict, raw }
228
+ return outer
229
+ }
230
+
231
+ /**
232
+ * Match a balanced open/close pair, skipping over any holes inside.
233
+ * Returns the full matched text including delimiters.
234
+ *
235
+ * const parenGroup = balanced('(', ')', { skip: [comment, stringLit] })
236
+ *
237
+ * With no per-call `skip`, a balanced under a grammar that declares
238
+ * `rules({ scanSkip })` consults that ambient opaque-unit set in its INTERIOR too,
239
+ * so a delimiter hidden inside a string/bracket run never closes the balance
240
+ * early — the same footgun closure `scanTo` gets. `raw: true` opts out.
241
+ *
242
+ * By DEFAULT an unmatched close is RECOVERED, not failed: the close is wrapped in
243
+ * `expect()`, so the group always succeeds and records a ParseError instead. Pass
244
+ * `strict: true` to make it fail and roll back — see `ScanToOptions.strict`.
245
+ */
246
+ export function balanced(
247
+ open: string,
248
+ close: string,
249
+ options: ScanToOptions = {},
250
+ ): Combinator<string> {
251
+ const ownSkip = options.skip ?? []
252
+ const strict = options.strict ?? false
253
+ const combi = buildBalancedInterior(open, close, ownSkip, strict)
254
+ // `raw` keeps the pre-ambient behavior: the eager interior (per-call skip only).
255
+ if (options.raw) return markSpec(token(combi), open, close, ownSkip, strict, true)
256
+
257
+ // Ambient-aware in place — the returned combinator KEEPS its identity (its own
258
+ // interior `self` ref points back to it, and ir-serialize / codegen dedup rely
259
+ // on that), so we override `parse` rather than wrapping. `_def`/`_meta` are
260
+ // unchanged, so every static analysis sees the eager interior; only PARSING and
261
+ // codegen EMIT re-resolve `ctx.scanSkip`. Interiors are cached by the ambient
262
+ // array's identity (stable per grammar). Balanced consults ambient `scanSkip`
263
+ // (opaque units) but NOT trivia — its delimiters are structural and its content
264
+ // regex already spans whitespace, so adding trivia would perturb every existing
265
+ // balanced; the footgun is a delimiter hidden in opaque-unit content.
266
+ const eagerParse = combi.parse.bind(combi)
267
+ const cache = new Map<readonly Combinator<unknown>[], Combinator<string>>()
268
+ ;(combi as BalancedAmbient)._balancedAmbient = { open, close, ownSkip, strict }
269
+ combi.parse = (input: string, pos: number, ctx: ParseContext): ParseResult<string> => {
270
+ const amb = ctx.scanSkip
271
+ if (!amb || amb.length === 0) return eagerParse(input, pos, ctx)
272
+ let interior = cache.get(amb)
273
+ if (!interior) {
274
+ interior = buildBalancedInterior(open, close, [...amb, ...ownSkip], strict)
275
+ cache.set(amb, interior)
276
+ }
277
+ return interior.parse(input, pos, ctx)
278
+ }
279
+ /**
280
+ * ONE LEAF, like `scanTo` — the sibling in this file that has always got this
281
+ * right.
282
+ *
283
+ * `balanced` is declared `Combinator<string>` and its interior callback
284
+ * reassembles the whole match into exactly that one string. But the interior is
285
+ * spelled `transform(sequence(literal(open), many(...), expect(literal(close))))`
286
+ * and `transform` is TRANSPARENT to CST capture, so the reassembled string never
287
+ * reached the parent's `children`: `balanced('(', ')')` over `"(a(b)c)"`
288
+ * contributed SEVEN children while `scanTo` in the same file contributed one.
289
+ * The declared type and the emitted arity disagreed, and nothing checked.
290
+ *
291
+ * `token()` is the fix rather than a bespoke wrapper because both engines
292
+ * already understand `tag: 'token'` — the interpreter suppresses the interior
293
+ * collectors and pushes one leaf, and codegen emits the mirror. Identity is
294
+ * preserved where it matters: `_balancedAmbient` and the interior `self`
295
+ * back-edge stay on the INNER combinator, which is what codegen's ambient
296
+ * `scanSkip` rebuild reads and what nested opens recurse into — so a nested
297
+ * balanced still contributes nothing of its own, and only the outermost match
298
+ * becomes a leaf.
299
+ *
300
+ * This is the same rule the separated-list change enforces: a combinator may
301
+ * collapse only what its construction makes recoverable. `balanced`'s
302
+ * delimiters are FIXED at construction, so collapsing to one string is
303
+ * legitimate here — which is exactly why it must actually collapse.
304
+ */
305
+ return markSpec(token(combi), open, close, ownSkip, strict, false)
306
+ }
307
+
308
+ /** Build a balanced open/close interior for a FIXED skip set (no ambient). */
309
+ export function buildBalancedInterior(
310
+ open: string,
311
+ close: string,
312
+ skips: Combinator<unknown>[],
313
+ strict = false,
314
+ ): Combinator<string> {
315
+ // The interior scan must skip NESTED same-delimiter pairs so depth is counted —
316
+ // otherwise `{{x}}` stops at the first `}`. `self` references this balanced
317
+ // combinator; added to the interior scan's skip list, a nested `open` is
318
+ // consumed intact (recursively) before the scan looks for the matching `close`.
319
+ const self = ref<string>()
320
+ // PREDICTIVE interior — no char-walk. The body is `many(choice(self, …skips,
321
+ // contentRun))`, where contentRun is a regex of chars that are NOT this pair's
322
+ // delimiters and NOT the start of any skip (so a string/comment arm still wins
323
+ // its position). At any other character — a stray close, or a *different* bracket
324
+ // type — no arm matches, `many` stops, and the required close (expect()) reports
325
+ // "expected <close>". So an unmatched open, a cross-type close `(a]`, and a stray
326
+ // close all surface as errors; nothing is silently swallowed. Well-formed input
327
+ // is consumed identically.
328
+ const stop = new Set<string>([open, close])
329
+ let bounded = true
330
+ for (const sk of skips) {
331
+ const cs = firstSetClassChars(sk)
332
+ if (cs === null) { bounded = false; break }
333
+ for (const ch of cs) stop.add(ch)
334
+ }
335
+ const cls = [...stop].map(escapeClassChar).join('')
336
+ // A run when every skip's start is bounded (fast); else one char at a time so a
337
+ // skip arm always gets the chance to match at its position.
338
+ const content = bounded
339
+ ? regex(new RegExp(`[^${cls}]+`))
340
+ : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`))
341
+ const inner = many(choice(self, ...skips, content))
342
+ // The close is the ONLY thing strict mode changes. `expect()` recovers — it
343
+ // returns a ParseError and lets the group succeed anyway — so the default
344
+ // balanced can never fail past its opener. A bare `literal(close)` fails the
345
+ // sequence instead, which rolls the whole group back to the opener and lets an
346
+ // enclosing choice()/not()/attempt() actually see the rejection. Everything
347
+ // else (first-set gating, trivia, the skip set, the content run, the interior
348
+ // recursion through `self`) is shared, so strict and tolerant differ in
349
+ // failure behaviour only, never in what they accept.
350
+ const closer = strict ? literal(close) : expect(literal(close))
351
+ const combi = transform(
352
+ sequence(literal(open), inner, closer),
353
+ // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
354
+ // ParseError (recovered close). `c` is the close string or a ParseError.
355
+ ([o, parts, c]) => o + (parts as unknown[]).map(p => typeof p === 'string' ? p : Array.isArray(p) ? p.join('') : '').join('') + (typeof c === 'string' ? c : ''),
356
+ )
357
+ // Provide the callback source so the macro can inline this library-internal
358
+ // transform (codegen derives map-fn sources from def.fnSrc).
359
+ if (combi._def.tag === 'transform') {
360
+ combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")'
361
+ // This is Parseman's structural delimiter reconstruction, not a grammar
362
+ // semantic reduction. It must remain eligible for recognition-only
363
+ // composeLeaf artifacts while user-authored transform() remains excluded.
364
+ combi._def.recognitionOnly = true
365
+ }
366
+ // Record what this interior IS, before the self-ref closes the cycle. Any walker
367
+ // that would otherwise descend through `self` forever can stop here and render
368
+ // the construct instead of its lowering.
369
+ ;(combi as BalancedMarked)._balanced = { open, close }
370
+ self.define(combi as Combinator<string>)
371
+ return combi
372
+ }
373
+
374
+ /**
375
+ * The characters a combinator can START with, as char-class members — or null if
376
+ * its first set is unbounded ('any') or too broad to be a delimiter. Used by
377
+ * balanced() to keep a content run from eating the start of a skip.
378
+ */
379
+ function firstSetClassChars(c: Combinator<unknown>): string[] | null {
380
+ const fs = c._meta.firstSet
381
+ if (fs.kind === 'empty') return []
382
+ if (fs.kind !== 'ranges') return null
383
+ const out: string[] = []
384
+ for (const { lo, hi } of fs.ranges) {
385
+ if (hi - lo > 8) return null
386
+ for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp))
387
+ }
388
+ return out
389
+ }
390
+
391
+ function escapeClassChar(ch: string): string {
392
+ return ch.replace(/[\\\]^-]/g, '\\$&')
393
+ }
@@ -0,0 +1,221 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseFail } from '../types.ts'
2
+ import { sequenceFirstSet, firstSetOf, union } from './first-set.ts'
3
+ import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTrivia } from './trivia-skip.ts'
4
+ import { firstSetSentinel } from '../recovery/scan.ts'
5
+ import { adjacencyFail, adjacencyOf, holdsAdjacency, type AdjacencyDef } from './adjacency.ts'
6
+
7
+ type UnwrapParsers<T extends Combinator<unknown>[]> = {
8
+ [K in keyof T]: T[K] extends Combinator<infer U> ? U : never
9
+ }
10
+
11
+ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[]]>(
12
+ ...parsers: T
13
+ ): Combinator<UnwrapParsers<T>> {
14
+ // Adjacency markers (`adjacent()` / `notAdjacent()`) are BOUNDARY tests, lowered
15
+ // here rather than parsed as terms. At index 0 there is no preceding term in this
16
+ // sequence, and the gap before `pos` belongs to whoever called us — locally
17
+ // undecidable, so reject it at construction instead of answering it wrongly.
18
+ // Stays `null` for the overwhelming majority of sequences, which is the point:
19
+ // every grammar builds thousands of these, and a per-sequence array to say "no
20
+ // assertions here" would be a permanent retained allocation for a fact that is
21
+ // almost always no.
22
+ let adjacency: (AdjacencyDef | null)[] | null = null
23
+ for (let i = 0; i < parsers.length; i++) {
24
+ const a = adjacencyOf(parsers[i]!)
25
+ if (a === null) continue
26
+ if (i === 0) {
27
+ throw new TypeError(
28
+ `sequence(): ${a.polarity}() cannot be the FIRST term — an adjacency assertion tests the `
29
+ + 'gap after the PRECEDING term. Put a concrete term first.',
30
+ )
31
+ }
32
+ ;(adjacency ??= Array.from({ length: parsers.length }, () => null))[i] = a
33
+ }
34
+ const hasAdjacency = adjacency !== null
35
+
36
+ const meta: ParserMeta = {
37
+ // Union through the nullable prefix — a leading optional()/many() lets a later
38
+ // term's first char start the sequence. Just `parsers[0]` under-approximates.
39
+ firstSet: sequenceFirstSet(parsers),
40
+ canMatchNewline: parsers.some(p => p._meta.canMatchNewline),
41
+ isTrivia: false,
42
+ }
43
+
44
+ const def: { tag: 'sequence'; parsers: Combinator<unknown>[]; valueUnused?: boolean } =
45
+ { tag: 'sequence', parsers: parsers as Combinator<unknown>[] }
46
+
47
+ // Automatic recovery-sync inference, built lazily on the first tolerant parse and
48
+ // never touched on the strict path: followSentinels[i] matches (zero-width) when
49
+ // the input could start any term AFTER i, so a list nested in term i can resync
50
+ // to this sequence's enclosing delimiter with NO grammar annotation. This is the
51
+ // whole of "recovery config" — derived from structure, not authored.
52
+ let followSentinels: (Combinator<null> | null)[] | undefined
53
+
54
+ // Tolerant twin of the strict loop: identical term-parsing, but around each term
55
+ // it publishes the inferred follow sentinel into ctx._sync so a nested list can
56
+ // resync to this sequence's enclosing delimiter. Cold path (tolerant only).
57
+ function parseTolerant(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
58
+ followSentinels ??= parsers.map((_, i) => {
59
+ // A nested list can resync to the start of ANY term that follows it here, so
60
+ // union every following term's first set (not just up to the first
61
+ // non-nullable one — a mandatory middle term must not hide a later close).
62
+ const fs = parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
63
+ (acc, p) => union(acc, firstSetOf(p)),
64
+ { kind: 'empty' },
65
+ )
66
+ return firstSetSentinel(fs)
67
+ })
68
+ const values: unknown[] | undefined = def.valueUnused ? undefined : []
69
+ let cur = pos
70
+ const inheritedSync = ctx._sync
71
+ try {
72
+ for (let i = 0; i < parsers.length; i++) {
73
+ // Publish this term's follow set (or keep the inherited sync when the local
74
+ // follow isn't usable, e.g. the last term or an `any` first set).
75
+ ctx._sync = followSentinels[i] ?? inheritedSync
76
+ const adj = adjacency === null ? null : adjacency[i]
77
+ if (adj) {
78
+ if (!holdsAdjacency(input, cur, ctx, adj)) return adjacencyFail(cur, adj)
79
+ if (values !== undefined) values.push(null)
80
+ continue
81
+ }
82
+ if (ctx.trivia && i > 0) {
83
+ const mark = saveTriviaMark(ctx)
84
+ let scanEnd: number
85
+ if (needsDeferredTriviaCommit(ctx)) {
86
+ const scan = scanTrivia(input, cur, ctx)
87
+ scan.commit()
88
+ scanEnd = scan.end
89
+ } else {
90
+ scanEnd = advanceTrivia(input, cur, ctx)
91
+ }
92
+ const result = parsers[i]!.parse(input, scanEnd, ctx)
93
+ if (!result.ok) return result as ParseFail
94
+ if (result.span.end > scanEnd) cur = result.span.end
95
+ else rollbackTrivia(ctx, mark)
96
+ if (values !== undefined) values.push(result.value)
97
+ continue
98
+ }
99
+ const result = parsers[i]!.parse(input, cur, ctx)
100
+ if (!result.ok) return result as ParseFail
101
+ if (values !== undefined) values.push(result.value)
102
+ cur = result.span.end
103
+ }
104
+ } finally {
105
+ ctx._sync = inheritedSync
106
+ }
107
+ return {
108
+ ok: true,
109
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
110
+ value: (values ?? undefined) as UnwrapParsers<T>,
111
+ span: { start: pos, end: cur },
112
+ }
113
+ }
114
+
115
+ // Strict twin of the loop below, for the (rare) sequence that carries an adjacency
116
+ // assertion. FORKED rather than branched inline so the ordinary strict loop — the
117
+ // hot path of every grammar — keeps exactly the instructions it had before.
118
+ function parseAdjacent(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
119
+ const values: unknown[] | undefined = def.valueUnused ? undefined : []
120
+ let cur = pos
121
+
122
+ for (let i = 0; i < parsers.length; i++) {
123
+ const adj = adjacency![i]
124
+ if (adj) {
125
+ // Assert over the gap at `cur` and move nothing. The NEXT term re-scans the
126
+ // same gap and owns the commit/rewind decision, so the tree, the spans and
127
+ // the trivia log are byte-identical to the same sequence without the marker.
128
+ if (!holdsAdjacency(input, cur, ctx, adj)) return adjacencyFail(cur, adj)
129
+ if (values !== undefined) values.push(null)
130
+ continue
131
+ }
132
+ if (ctx.trivia && i > 0) {
133
+ let scanEnd: number
134
+ const mark = saveTriviaMark(ctx)
135
+ if (needsDeferredTriviaCommit(ctx)) {
136
+ const scan = scanTrivia(input, cur, ctx)
137
+ scan.commit()
138
+ scanEnd = scan.end
139
+ } else {
140
+ scanEnd = advanceTrivia(input, cur, ctx)
141
+ }
142
+ const result = parsers[i]!.parse(input, scanEnd, ctx)
143
+ if (!result.ok) return result as ParseFail
144
+ if (result.span.end > scanEnd) cur = result.span.end
145
+ else rollbackTrivia(ctx, mark)
146
+ if (values !== undefined) values.push(result.value)
147
+ continue
148
+ }
149
+ const result = parsers[i]!.parse(input, cur, ctx)
150
+ if (!result.ok) return result as ParseFail
151
+ if (values !== undefined) values.push(result.value)
152
+ cur = result.span.end
153
+ }
154
+
155
+ return {
156
+ ok: true,
157
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
158
+ value: (values ?? undefined) as UnwrapParsers<T>,
159
+ span: { start: pos, end: cur },
160
+ }
161
+ }
162
+
163
+ return {
164
+ _tag: 'sequence',
165
+ _meta: meta,
166
+ _def: def,
167
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
168
+ // One cold branch: in tolerant mode publish each term's follow-set as ctx._sync
169
+ // so a nested list resyncs to the enclosing delimiter (dynamic scoping through
170
+ // refs carries this across rule boundaries automatically). The strict loop
171
+ // below is byte-identical to a parser with no recovery.
172
+ if (ctx._tolerant) return parseTolerant(input, pos, ctx)
173
+ if (hasAdjacency) return parseAdjacent(input, pos, ctx)
174
+
175
+ // Skip the tuple when it's never observed (markUnusedValues): terms still
176
+ // parse (and self-capture) — only the array of their values is elided.
177
+ const values: unknown[] | undefined = def.valueUnused ? undefined : []
178
+ let cur = pos
179
+
180
+ for (let i = 0; i < parsers.length; i++) {
181
+ if (ctx.trivia && i > 0) {
182
+ // Skip trivia between terms, but only *consume* it for span purposes if
183
+ // this term actually matches content past the trivia. A term that matches
184
+ // empty (optional/many/lookahead) leaves the trivia for the enclosing rule.
185
+ let scanEnd: number
186
+ let mark = saveTriviaMark(ctx)
187
+
188
+ if (needsDeferredTriviaCommit(ctx)) {
189
+ const scan = scanTrivia(input, cur, ctx)
190
+ scan.commit()
191
+ scanEnd = scan.end
192
+ } else {
193
+ scanEnd = advanceTrivia(input, cur, ctx)
194
+ }
195
+
196
+ const result = parsers[i]!.parse(input, scanEnd, ctx)
197
+ if (!result.ok) return result as ParseFail
198
+ if (result.span.end > scanEnd) {
199
+ cur = result.span.end
200
+ } else {
201
+ rollbackTrivia(ctx, mark)
202
+ }
203
+ if (values !== undefined) values.push(result.value)
204
+ continue
205
+ }
206
+
207
+ const result = parsers[i]!.parse(input, cur, ctx)
208
+ if (!result.ok) return result as ParseFail
209
+ if (values !== undefined) values.push(result.value)
210
+ cur = result.span.end
211
+ }
212
+
213
+ return {
214
+ ok: true,
215
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
216
+ value: (values ?? undefined) as UnwrapParsers<T>,
217
+ span: { start: pos, end: cur },
218
+ }
219
+ },
220
+ }
221
+ }