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,322 @@
1
+ import type { CharRange, FirstSet, Combinator, ParserDef } from '../types.ts'
2
+
3
+ /**
4
+ * Resolve a NAMED cross-artifact rule reference (`g.Foo`) whose own thunk does not
5
+ * resolve — the shared-shape hole, bound by name at fuse time.
6
+ *
7
+ * A shape module compiles `Ratio: sequence(g.Value, …)` without defining `Value`, so
8
+ * that `lazy`'s thunk throws and every first-set through it degrades to `any`. Once
9
+ * the shape is FUSED with a dialect that defines `Value`, the hole is bound — pass a
10
+ * resolver over the fused winner map and the first-set is the real one.
11
+ *
12
+ * Only NAMED refs are resolvable: an unnamed `ref()` that was never `.define()`d
13
+ * carries no name for anyone to bind, so it stays `any`.
14
+ */
15
+ export type RefResolver = (name: string) => Combinator<unknown> | undefined
16
+
17
+ /** The `resolve`-supplied target for an unresolvable NAMED lazy, if any. */
18
+ function resolveNamedRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
19
+ if (resolve === undefined) return undefined
20
+ const name = (p as unknown as { _ruleName?: string })._ruleName
21
+ return name === undefined ? undefined : resolve(name)
22
+ }
23
+
24
+ export function union(a: FirstSet, b: FirstSet): FirstSet {
25
+ if (a.kind === 'any' || b.kind === 'any') return { kind: 'any' }
26
+ if (a.kind === 'empty') return b
27
+ if (b.kind === 'empty') return a
28
+ return { kind: 'ranges', ranges: mergeRanges([...a.ranges, ...b.ranges]) }
29
+ }
30
+
31
+ export function intersects(a: FirstSet, b: FirstSet): boolean {
32
+ if (a.kind === 'any' || b.kind === 'any') return true
33
+ if (a.kind === 'empty' || b.kind === 'empty') return false
34
+ for (const ra of a.ranges) {
35
+ for (const rb of b.ranges) {
36
+ if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true
37
+ }
38
+ }
39
+ return false
40
+ }
41
+
42
+ export function fromChar(code: number): FirstSet {
43
+ return { kind: 'ranges', ranges: [{ lo: code, hi: code }] }
44
+ }
45
+
46
+ /**
47
+ * True when `combinator`'s first set admits the code point at `input[pos]` (or its
48
+ * first set is `any`). The runtime counterpart of codegen's `firstSetCond` guard —
49
+ * used by the interpreter's first-set fail-fast in `optional`/`many`/`attempt`/
50
+ * `node` to reject a doomed sub-parse before doing any setup. Returns `false` at EOF.
51
+ */
52
+ export function startsFirstSet(combinator: Combinator<unknown>, input: string, pos: number): boolean {
53
+ const fs = combinator._meta.firstSet
54
+ if (fs.kind === 'any') return true
55
+ if (fs.kind === 'empty') return false
56
+ const code = input.codePointAt(pos)
57
+ if (code === undefined) return false
58
+ for (const r of fs.ranges) if (code >= r.lo && code <= r.hi) return true
59
+ return false
60
+ }
61
+
62
+ export function fromRange(lo: number, hi: number): FirstSet {
63
+ return { kind: 'ranges', ranges: [{ lo, hi }] }
64
+ }
65
+
66
+ export function any(): FirstSet {
67
+ return { kind: 'any' }
68
+ }
69
+
70
+ export function empty(): FirstSet {
71
+ return { kind: 'empty' }
72
+ }
73
+
74
+ /**
75
+ * Can this parser SUCCEED consuming zero characters (nullable / matches-empty)?
76
+ * Used to compute a sound sequence first-set: a nullable leading term lets the
77
+ * NEXT term's first chars start the whole sequence. MUST err toward `true` when
78
+ * unsure — over-estimating nullability only widens the (over-approximated) first
79
+ * set, which stays sound; under-estimating would drop valid start chars and make
80
+ * first-char dispatch skip a matching arm.
81
+ */
82
+ export function matchesEmpty(
83
+ p: Combinator<unknown>,
84
+ seen: Set<Combinator<unknown>> = new Set(),
85
+ resolve?: RefResolver,
86
+ ): boolean {
87
+ // Cycle guard: a mutually-nullable ref cycle (e.g. `A = oneOrMore(B); B = oneOrMore(A)`)
88
+ // would recurse forever. Treat a re-entered node as nullable — the safe (`true`)
89
+ // default, consistent with the err-toward-true contract below.
90
+ if (seen.has(p)) return true
91
+ seen.add(p)
92
+ const me = (c: Combinator<unknown>): boolean => matchesEmpty(c, seen, resolve)
93
+ const d = p._def as ParserDef
94
+ switch (d.tag) {
95
+ case 'literal': return d.value.length === 0
96
+ case 'keywords': return false
97
+ // The routed token itself is the selector's match, never empty; a fallback can be.
98
+ case 'routed': return d.fallback === undefined ? false : me(d.fallback)
99
+ case 'regex':
100
+ // Precise: does the pattern admit a zero-length match? (`a*`, `a?`, `a|`, …)
101
+ try { const m = new RegExp(d.source).exec(''); return m != null && m[0] === '' }
102
+ catch { return true }
103
+ case 'many':
104
+ case 'optional':
105
+ case 'not':
106
+ case 'peek': return true // zero repetitions / absent / lookahead
107
+ // Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
108
+ // Any `min >= 1` requires that many ITEMS, so it is nullable only when the
109
+ // item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
110
+ // nullable — safe, but wrong, and it put a bogus `nullable-prefix` note on
111
+ // the gating diagnostic for a list that can never match empty.)
112
+ case 'sepBy': return d.min >= 1 ? me(d.parser) : true
113
+ case 'oneOrMore': return me(d.parser)
114
+ case 'sequence': return d.parsers.every(me)
115
+ case 'choice':
116
+ return d.parsers.some(me)
117
+ case 'dispatch':
118
+ return me(d.selector)
119
+ case 'transform':
120
+ case 'label':
121
+ case 'trivia':
122
+ case 'token':
123
+ case 'leaf':
124
+ case 'expect':
125
+ case 'withCtx':
126
+ case 'node':
127
+ case 'grammar':
128
+ case 'recover': return me(d.parser)
129
+ case 'lazy':
130
+ try { return me(d.thunk()) }
131
+ catch { const t = resolveNamedRef(p, resolve); return t ? me(t) : true }
132
+ default: return true // scanTo / guard / unknown → assume nullable (safe)
133
+ }
134
+ }
135
+
136
+ /**
137
+ * A ZERO-WIDTH ASSERTION never consumes input, so it contributes NOTHING to a
138
+ * sequence's first-set — the first consumed char comes from the following
139
+ * non-nullable term. `not(X)` reports `firstSet: any()` (it cannot know what it
140
+ * forbids), which would otherwise poison a sequence's first-set to `any` and kill
141
+ * first-char dispatch of the whole arm. Skipping its contribution is SOUND: a
142
+ * first-set used for dispatch gating must stay a correct SUPERSET of the rule's
143
+ * true first chars, and `not(X) Y` can only start with a char in firstSet(Y) — the
144
+ * assertion only NARROWS the language (it forbids a full match ahead), it never
145
+ * widens the set of possible first chars beyond Y. So firstSet(Y) is a sound (and
146
+ * tighter) superset.
147
+ *
148
+ * The POSITIVE lookahead `peek(X)` is zero-width too, but it is NOT in this
149
+ * predicate: unlike `not`, it knows what it requires, so its first-set is a real
150
+ * constraint that must be INTERSECTED into the sequence's set rather than dropped
151
+ * (see `isPositiveLookahead` and `sequenceFirstSet`).
152
+ */
153
+ export function isZeroWidthAssertion(p: Combinator<unknown>): boolean {
154
+ const tag = (p._def as ParserDef).tag
155
+ // `adjacency` joins `not` for the same reason: `adjacent()`/`notAdjacent()` are
156
+ // zero-width tests of the gap BEHIND the cursor, so they constrain nothing about
157
+ // the first char ahead and must not contribute their `any` to the sequence.
158
+ return tag === 'not' || tag === 'adjacency'
159
+ }
160
+
161
+ /**
162
+ * A POSITIVE zero-width assertion (`peek(X)`). It consumes nothing, so like
163
+ * `not(X)` it does not contribute a first char of its own — but it does REQUIRE
164
+ * that X match here, so `peek(X) Y` can only start with a char in
165
+ * firstSet(X) ∩ firstSet(Y). That intersection is what makes a leading `peek()`
166
+ * gate its choice arm; `not(not(X))`, the only previous spelling, reports `any()`
167
+ * and poisons the dispatch instead.
168
+ *
169
+ * Soundness: first-sets are SUPERSETS of the true first chars, and
170
+ * (A ⊇ a) ∧ (B ⊇ b) ⇒ A ∩ B ⊇ a ∩ b — so intersecting stays a superset and can
171
+ * never skip a real match. A NULLABLE body succeeds on the empty string and
172
+ * therefore constrains nothing; `peek()` reports `any()` in that case, which the
173
+ * intersection treats as "no constraint".
174
+ */
175
+ export function isPositiveLookahead(p: Combinator<unknown>): boolean {
176
+ return (p._def as ParserDef).tag === 'peek'
177
+ }
178
+
179
+ /** Intersect a lookahead constraint into an accumulator; `any` = no constraint. */
180
+ function narrowBy(acc: FirstSet | null, constraint: FirstSet): FirstSet | null {
181
+ if (constraint.kind === 'any') return acc
182
+ if (acc === null) return constraint
183
+ if (acc.kind === 'any') return constraint
184
+ if (acc.kind === 'empty' || constraint.kind === 'empty') return { kind: 'empty' }
185
+ const ranges: CharRange[] = []
186
+ for (const a of acc.ranges) for (const b of constraint.ranges) {
187
+ const lo = Math.max(a.lo, b.lo)
188
+ const hi = Math.min(a.hi, b.hi)
189
+ if (lo <= hi) ranges.push({ lo, hi })
190
+ }
191
+ return ranges.length === 0 ? { kind: 'empty' } : { kind: 'ranges', ranges }
192
+ }
193
+
194
+ /**
195
+ * Apply the accumulated `peek()` constraints to a sequence's first-set. When the
196
+ * consuming terms contributed NOTHING (the sequence is all zero-width/nullable,
197
+ * e.g. a bare `peek(X)` arm), the assertion IS the first-set: the sequence can
198
+ * only succeed — even zero-width — where X matches.
199
+ */
200
+ function applyAssertion(fs: FirstSet, assertion: FirstSet | null): FirstSet {
201
+ if (assertion === null) return fs
202
+ if (fs.kind === 'empty') return assertion
203
+ return narrowBy(fs, assertion) ?? fs
204
+ }
205
+
206
+ /**
207
+ * First-set of a sequence: union each term's first-set through the NULLABLE
208
+ * PREFIX — a leading `optional(…)` / `many(…)` / nullable term can be skipped, so
209
+ * the sequence can begin with a LATER term's first char. Stop at (and include)
210
+ * the first non-nullable term. (`parsers[0].firstSet` alone under-approximates
211
+ * and silently breaks first-char dispatch — see the InterpolatedSelector bug.)
212
+ * A leading zero-width assertion (`not(…)`) is nullable but contributes NOTHING to
213
+ * the first-set (see `isZeroWidthAssertion`) — its `any` must not poison the union.
214
+ */
215
+ export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
216
+ let fs: FirstSet = empty()
217
+ let assertion: FirstSet | null = null
218
+ for (const p of parsers) {
219
+ if (isPositiveLookahead(p)) {
220
+ // Zero-width but CONSTRAINING: intersect, keep scanning (it consumes nothing).
221
+ assertion = narrowBy(assertion, p._meta.firstSet)
222
+ continue
223
+ }
224
+ if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet)
225
+ if (!matchesEmpty(p)) return applyAssertion(fs, assertion)
226
+ }
227
+ return applyAssertion(fs, assertion)
228
+ }
229
+
230
+ /**
231
+ * Deep first-set that RESOLVES `lazy`/`ref` combinators to their targets. The
232
+ * combinators bake `_meta.firstSet` at CONSTRUCTION, when a `ref()` still reads
233
+ * `any()` (define() never updates it) — so a `choice`/`sequence` built over refs
234
+ * caches a spuriously-`any` first-set and loses first-char dispatch. Recomputing
235
+ * here, following refs, recovers the real set. Over-approximates on cycles /
236
+ * unknown constructs (returns `any`) — always sound: a wider set only means "try
237
+ * this arm for more first chars", never skips a real match.
238
+ *
239
+ * SOUND ONLY where refs are FINAL (monolithic compile). Under compose OVERRIDE a
240
+ * referenced rule can be replaced with a WIDER first-set, so a baked deep set
241
+ * would wrongly skip valid input — the compose path defers dispatch to fuse time.
242
+ *
243
+ * `resolve` binds NAMED cross-artifact holes (`g.Foo`) against a fused winner map —
244
+ * see `RefResolver`. Diagnostic-only today: it is what lets the gating analysis ask
245
+ * the question at the site where the hole actually HAS an answer.
246
+ */
247
+ export function firstSetOf(
248
+ p: Combinator<unknown>,
249
+ seen: Set<Combinator<unknown>> = new Set(),
250
+ resolve?: RefResolver,
251
+ ): FirstSet {
252
+ if (seen.has(p)) return any() // cycle → any (safe over-approximation)
253
+ seen.add(p)
254
+ const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve)
255
+ const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
256
+ const d = p._def as ParserDef
257
+ switch (d.tag) {
258
+ case 'literal':
259
+ case 'regex':
260
+ case 'keywords': return p._meta.firstSet // terminals: no refs, cached set is exact
261
+ case 'dispatch': return fs(d.selector)
262
+ case 'lazy':
263
+ try { return fs(d.thunk()) }
264
+ catch { const t = resolveNamedRef(p, resolve); return t ? fs(t) : any() }
265
+ case 'choice': {
266
+ let out: FirstSet = empty()
267
+ for (const arm of d.parsers) out = union(out, fs(arm))
268
+ return out
269
+ }
270
+ case 'sequence': {
271
+ // Union through the nullable prefix (a leading nullable term lets a later
272
+ // term's first chars start the sequence) — ref-resolving `sequenceFirstSet`.
273
+ // A leading zero-width assertion (`not`) contributes nothing (its `any` would
274
+ // poison the union) but is still nullable, so keep scanning past it. A
275
+ // positive `peek()` is zero-width AND constraining → intersect.
276
+ let out: FirstSet = empty()
277
+ let assertion: FirstSet | null = null
278
+ for (const term of d.parsers) {
279
+ if (isPositiveLookahead(term)) { assertion = narrowBy(assertion, fs(term)); continue }
280
+ if (!isZeroWidthAssertion(term)) out = union(out, fs(term))
281
+ if (!empties(term)) return applyAssertion(out, assertion)
282
+ }
283
+ return applyAssertion(out, assertion)
284
+ }
285
+ case 'peek': {
286
+ // Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
287
+ // shallow `_meta.firstSet` baked into the assertion would lose the gate.
288
+ const inner = d.parser
289
+ return empties(inner) ? any() : fs(inner)
290
+ }
291
+ case 'oneOrMore':
292
+ case 'many':
293
+ case 'optional':
294
+ case 'transform':
295
+ case 'label':
296
+ case 'trivia':
297
+ case 'token':
298
+ case 'leaf':
299
+ case 'node':
300
+ case 'grammar':
301
+ case 'expect': return fs(d.parser)
302
+ case 'sepBy': return fs(d.parser) // both min 0 and min 1 start with the item
303
+ default: return p._meta.firstSet // not / scanTo / guard / withCtx / recover / unknown
304
+ }
305
+ }
306
+
307
+ function mergeRanges(ranges: CharRange[]): CharRange[] {
308
+ if (ranges.length === 0) return []
309
+ const sorted = [...ranges].sort((a, b) => a.lo - b.lo)
310
+ // Always copy — never alias input objects
311
+ const out: CharRange[] = [{ lo: sorted[0]!.lo, hi: sorted[0]!.hi }]
312
+ for (let i = 1; i < sorted.length; i++) {
313
+ const top = out[out.length - 1]!
314
+ const cur = sorted[i]!
315
+ if (cur.lo <= top.hi + 1) {
316
+ if (cur.hi > top.hi) top.hi = cur.hi
317
+ } else {
318
+ out.push({ lo: cur.lo, hi: cur.hi })
319
+ }
320
+ }
321
+ return out
322
+ }
@@ -0,0 +1,50 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+
3
+ /**
4
+ * Zero-width state ASSERTION: succeeds (consuming nothing) only when `predicate`
5
+ * returns true for `ctx.state`. Fails otherwise.
6
+ *
7
+ * Intended for use inside sequence() to gate subsequent parsing on runtime
8
+ * context set with withCtx():
9
+ *
10
+ * const returnStmt = sequence(
11
+ * gate(ctx => (ctx as { inFn: boolean }).inFn),
12
+ * literal('return'), optional(expr)
13
+ * )
14
+ *
15
+ * Naming: this matches the `gate:` field on a gated CHOICE arm
16
+ * (`choice({ gate, combinator }, other)`). Use the arm FIELD to SELECT a branch by
17
+ * a cheap state predicate; use this COMBINATOR to ASSERT a state predicate
18
+ * mid-sequence. Formerly named `guard()` — see the deprecated alias in `guard.ts`.
19
+ *
20
+ * Note: like all state predicates its first-set is `any`, so a `gate(...)` as the
21
+ * LEADING term of a choice arm poisons that choice's first-char dispatch. Keep it
22
+ * after a concrete leading terminal.
23
+ */
24
+ export function gate(predicate: (state: unknown) => boolean): Combinator<null> {
25
+ const meta: ParserMeta = {
26
+ firstSet: { kind: 'any' },
27
+ canMatchNewline: false,
28
+ isTrivia: false,
29
+ }
30
+ return {
31
+ _tag: 'guard',
32
+ _meta: meta,
33
+ _def: { tag: 'guard', predicate },
34
+ parse(_input: string, pos: number, ctx: ParseContext): ParseResult<null> {
35
+ if (predicate(ctx.state))
36
+ return { ok: true, value: null, span: { start: pos, end: pos } }
37
+ // The label is the PUBLIC name. It used to be `'guard'`, justified in a
38
+ // comment here as "parity with the compiled path" — which had not been true
39
+ // since the rename: codegen emitted `'gate'` and this emitted `'guard'`, so
40
+ // the same failing input reported a different expected set depending on
41
+ // which engine ran it. Nothing caught it because no test compared the two
42
+ // engines' expected sets on a state-scoped failure.
43
+ //
44
+ // `guard` was removed as a public name in 0.44.0, so it also pointed users
45
+ // at something they could no longer find. The def TAG stays `'guard'` —
46
+ // that is internal and the encoder switches on it.
47
+ return { ok: false, expected: ['gate'], span: { start: pos, end: pos } }
48
+ },
49
+ }
50
+ }
@@ -0,0 +1,272 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParseFail, ParseError } from '../types.ts'
2
+ import { createLineIndex, recordLineRange, normalizeLineIndex, annotateSpan, type LineIndex } from '../line-index.ts'
3
+ import { markUnusedValues } from '../compiler/value-usage.ts'
4
+ import { triviaKindMask } from '../cst/trivia-kinds.ts'
5
+ import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
6
+ import { createParseContext } from '../parse-context.ts'
7
+
8
+ export type ParseOptions = {
9
+ trackLines?: boolean
10
+ /**
11
+ * Enable the error-collection channel. When true, recovery points (expect(), and
12
+ * tolerant list recovery) collect their ParseErrors into a side-channel array
13
+ * rather than only embedding them in the value tree. The returned ParseOk will
14
+ * have an `errors` field listing all recovered errors. Top-level parse failures
15
+ * (where nothing recovered) still return ParseFail as usual.
16
+ */
17
+ recover?: boolean
18
+ }
19
+
20
+ export type ParserOptions = ParseOptions & {
21
+ /**
22
+ * Trivia parser for this scope. Three states:
23
+ * - a Combinator → skip this trivia between sequence/repeat terms
24
+ * - `undefined` → inherit the enclosing scope's trivia (no change)
25
+ * - `null` → CLEAR trivia: no trivia skipped, so terms must be contiguous.
26
+ * Re-enable inside a nested region with another parser({ trivia }).
27
+ */
28
+ trivia?: Combinator<unknown> | null
29
+ /**
30
+ * Declare that this local trivia scope intentionally does not preserve
31
+ * selected root categories. Required only by
32
+ * `rootTrivia: { select }` when the scope does not use
33
+ * `classifiedTrivia()`.
34
+ */
35
+ rootCapture?: 'opaque'
36
+ /** Record consumed trivia as CSTTrivia tokens in rawChildren. Default: skip. */
37
+ captureTrivia?: boolean
38
+ /**
39
+ * Restrict PER-NODE CST trivia capture to these trivia kinds (label names from
40
+ * the trivia's `label()` arms). Whitespace and any other unlisted kind is still
41
+ * skipped over but NOT recorded into a node's `triviaLog` — so a host that only
42
+ * consumes (say) comments pays nothing to log every whitespace run. The global
43
+ * trivia log is unaffected. Requires labeled trivia; ignored otherwise.
44
+ */
45
+ captureTriviaKinds?: readonly string[]
46
+ }
47
+
48
+ export interface ParsemanParser<T> extends Combinator<T> {
49
+ parse(input: string): ParseResult<T>
50
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T>
51
+ }
52
+
53
+ function annotateResultLines<T, R extends ParseResult<T>>(result: R, index: LineIndex): R {
54
+ const annotated = { ...result, span: annotateSpan(result.span, index) } as R
55
+ const errors = (annotated as { errors?: ParseError[] }).errors
56
+ if (errors) {
57
+ ;(annotated as { errors: ParseError[] }).errors = errors.map(error => ({
58
+ ...error,
59
+ span: annotateSpan(error.span, index),
60
+ }))
61
+ }
62
+ return annotated
63
+ }
64
+
65
+ function createParseLineContext(input: string, pos: number): { lineIndex: LineIndex; lineScannedTo: number } {
66
+ const lineIndex = createLineIndex()
67
+ if (pos > 0) recordLineRange(lineIndex, input, 0, pos)
68
+ return { lineIndex, lineScannedTo: pos }
69
+ }
70
+
71
+ /** The three pre-written `trackLines` resolvers. One is SELECTED at link time. */
72
+ const TRACK_LINES_ON = (): boolean => true
73
+ const TRACK_LINES_OFF = (): boolean => false
74
+ const TRACK_LINES_INHERIT = (ctx: ParseContext | undefined): boolean => ctx?.trackLines === true
75
+
76
+ /**
77
+ * A trivia scope.
78
+ *
79
+ * ── NO RULE MAY CONSULT AN OPTION WHILE IT PARSES ───────────────────────────
80
+ *
81
+ * `opts` is fixed when `parser(opts, root)` is called. That is LINK TIME — the
82
+ * same moment `assemble.ts` resolves `RunCfg` into a choice of piece, and the
83
+ * same rule (`RunCfg`'s header, `scripts/check-invariants.mjs` INV-6/INV-7).
84
+ * Everything below used to be read INSIDE `parse`, which a nested scope enters
85
+ * on every visit, so a grammar with a `parser({ trivia })` region paid nine
86
+ * option reads per entry for answers that could not change.
87
+ *
88
+ * They are now resolved once, here, and the parse path reads only the resolved
89
+ * values. Where the answer genuinely depends on the CALLER's context — an
90
+ * inherited `trackLines`, inherited trivia labels — a pre-written variant is
91
+ * SELECTED at link time rather than branched on per entry. Selecting costs one
92
+ * closure per scope in the grammar; branching cost one test per entry per parse.
93
+ *
94
+ * `_ctx.trivia`, `_ctx.captureTrivia` and the CST sinks stay where they are:
95
+ * those are per-scope RUNTIME state that `node()` opens and closes mid-parse,
96
+ * not configuration. Resolving them early would be wrong, not fast.
97
+ */
98
+ export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanParser<T> {
99
+ const clearTrivia = opts.trivia === null
100
+ const opaqueRootCapture = opts.rootCapture === 'opaque'
101
+ if (opaqueRootCapture && opts.trivia === undefined) {
102
+ throw new TypeError('parser({ rootCapture: \'opaque\' }) requires an explicit trivia scope.')
103
+ }
104
+ /** This scope's own trivia, or `undefined` for cleared/inherited. */
105
+ const scopeTrivia: Combinator<unknown> | undefined = clearTrivia ? undefined : (opts.trivia ?? undefined)
106
+ /**
107
+ * `?._meta?.` and not `?._meta.`: the macro's own evaluator builds a
108
+ * `parser({ trivia: /re/ })` from source text, so a raw RegExp reaches here.
109
+ * The reads used to happen inside `parse`, where such a scope threw only if it
110
+ * ever ran; hoisting them to construction must not turn that into a build-time
111
+ * throw. (`test/unit/plugin-coverage.test.ts`, "anyValue edge forms".)
112
+ */
113
+ const scopeLabels = scopeTrivia?._meta?.triviaKindLabels
114
+ /** Does this scope have to refuse an unclassified root scope? Link-time fact. */
115
+ const refuseUnclassified = scopeTrivia !== undefined
116
+ && !scopeTrivia._meta?.rootTriviaClassified && !opaqueRootCapture
117
+ const forceCaptureTrivia = opts.captureTrivia === true
118
+ const trackLinesOf = opts.trackLines === true ? TRACK_LINES_ON
119
+ : opts.trackLines === false ? TRACK_LINES_OFF
120
+ : TRACK_LINES_INHERIT
121
+ /**
122
+ * The per-node capture mask, resolved as far as this scope can resolve it.
123
+ *
124
+ * With its own labelled trivia the mask is a constant and is computed once.
125
+ * With INHERITED labels it depends on the caller, so the link step selects the
126
+ * deriving variant instead — `undefined` here means "no mask to install".
127
+ */
128
+ const captureKinds = clearTrivia ? undefined : opts.captureTriviaKinds
129
+ const ownCaptureMask = captureKinds === undefined ? undefined
130
+ : scopeLabels !== undefined ? triviaKindMask(scopeLabels, captureKinds)
131
+ : undefined
132
+ const inheritCaptureMask = captureKinds !== undefined && scopeLabels === undefined
133
+ return {
134
+ _tag: 'grammar',
135
+ _meta: {
136
+ ...root._meta,
137
+ // Trivia classification describes this grammar's own active trivia
138
+ // scope. A directly nested grammar may have classified trivia of its
139
+ // own, but that must not advertise classification for this wrapper.
140
+ triviaKindLabels: undefined,
141
+ rootTriviaClassified: undefined,
142
+ ...(opts.trivia?._meta?.triviaKindLabels ? { triviaKindLabels: opts.trivia._meta.triviaKindLabels } : {}),
143
+ ...(opts.trivia?._meta?.rootTriviaClassified ? { rootTriviaClassified: true as const } : {}),
144
+ },
145
+ _def: {
146
+ tag: 'grammar',
147
+ parser: root as Combinator<unknown>,
148
+ triviaParser: scopeTrivia,
149
+ clearTrivia,
150
+ ...(opaqueRootCapture ? { rootCapture: 'opaque' as const } : {}),
151
+ ...(opts.captureTrivia ? { captureTrivia: true } : {}),
152
+ trackLines: opts.trackLines ?? false,
153
+ },
154
+ parse(input: string, pos?: number, _ctx?: ParseContext): ParseResult<T> {
155
+ if (refuseUnclassified) refuseUnclassifiedRootScope(_ctx?._rootTriviaStrictScopes)
156
+ const trackLines = trackLinesOf(_ctx)
157
+ const lineContext = trackLines && _ctx?._lineIndex === undefined && _ctx?._lineStarts === undefined
158
+ ? createParseLineContext(input, pos ?? 0)
159
+ : undefined
160
+ const lineIndex = trackLines
161
+ ? (_ctx?._lineIndex ?? (_ctx?._lineStarts ? { lineStarts: _ctx._lineStarts } : lineContext!.lineIndex))
162
+ : undefined
163
+ // Preserve any CST collectors / capture flag from the caller (e.g. an
164
+ // enclosing node()), layering this grammar's trivia on top. Without this,
165
+ // a parser() nested inside a node() would drop the node's child collectors.
166
+ // Inheriting copy, then STORES — see `createParseContext`. `{ ..._ctx }`
167
+ // preserves the canonical shape because `_ctx` already has it, and the
168
+ // overrides below only touch slots that already exist.
169
+ const ctx: ParseContext = _ctx === undefined ? createParseContext() : { ..._ctx }
170
+ ctx.trackLines = trackLines
171
+ if (lineIndex) ctx._lineIndex = lineIndex
172
+ if (lineContext) ctx._lineScannedTo = lineContext.lineScannedTo
173
+ // trivia: null clears (contiguous terms); a Combinator sets; undefined inherits.
174
+ if (clearTrivia) {
175
+ ctx.trivia = undefined
176
+ ctx.triviaKindLabels = undefined
177
+ } else if (scopeTrivia !== undefined) {
178
+ ctx.trivia = scopeTrivia
179
+ if (scopeLabels) ctx.triviaKindLabels = scopeLabels
180
+ }
181
+ if (forceCaptureTrivia || _ctx?.captureTrivia) ctx.captureTrivia = true
182
+ // Kind-filter for per-node capture. Resolve against this scope's trivia
183
+ // labels — this parser's own trivia if it declares one, else the INHERITED
184
+ // labels (`_ctx.triviaKindLabels`), so captureTriviaKinds still applies when
185
+ // trivia is inherited rather than re-declared here. No labels → undefined
186
+ // (capture all).
187
+ if (ownCaptureMask !== undefined) ctx._triviaCaptureMask = ownCaptureMask
188
+ else if (inheritCaptureMask) ctx._triviaCaptureMask = triviaKindMask(_ctx?.triviaKindLabels, captureKinds)
189
+ // This is a property of the local trivia scope, not a post-parse filter:
190
+ // nested recognition can still skip its trivia, but selected root rows
191
+ // must never be written for an explicitly opaque region.
192
+ if (opaqueRootCapture) ctx._rootTriviaCapture = false
193
+ const result = root.parse(input, pos ?? 0, ctx)
194
+ if (trackLines && _ctx && ctx._lineScannedTo !== undefined) {
195
+ _ctx._lineScannedTo = Math.max(_ctx._lineScannedTo ?? 0, ctx._lineScannedTo)
196
+ }
197
+ return lineIndex ? annotateResultLines(result, normalizeLineIndex(lineIndex)) : result
198
+ },
199
+ } as ParsemanParser<T>
200
+ }
201
+
202
+ /**
203
+ * Run `root` with the active trivia cleared — no trivia is skipped between its
204
+ * sequence/repeat terms, so they must be contiguous in the input. (Trivia is
205
+ * whatever parser({ trivia }) installed — often whitespace/comments, but it is
206
+ * grammar-defined.) The inverse of parser({ trivia }); re-enable trivia in a
207
+ * nested region with another parser({ trivia }).
208
+ *
209
+ * Wrap the WHOLE contiguous run: an enclosing sequence skips trivia *before* a
210
+ * term runs, so wrapping just the inner part would let leading trivia through.
211
+ *
212
+ * For a static glued token just use one literal/regex; reach for noTrivia when a
213
+ * glued part is a structured sub-rule — e.g. a head glued to a `[subscript]`
214
+ * whose interior still allows trivia. Turn trivia back on for a region by nesting
215
+ * another parser({ trivia }) (innermost wins, reverts on exit); put the WHOLE
216
+ * spaced region — including its leading `[` — inside it, since sequence skips
217
+ * trivia only between terms, not before its first:
218
+ *
219
+ * // `arr[i + 1]` — `arr` touches `[`, but the subscript is a spaced expr:
220
+ * noTrivia(sequence(name,
221
+ * parser({ trivia: ws }, sequence(literal('['), expr, literal(']')))))
222
+ */
223
+ export function noTrivia<T>(root: Combinator<T>): ParsemanParser<T> {
224
+ return parser({ trivia: null }, root)
225
+ }
226
+
227
+ // Roots already run through dead-value analysis, so a hot parse loop doesn't
228
+ // re-walk the tree. (A `rules()` grammar was analyzed at build; a bare combinator
229
+ // passed straight to parse() is analyzed once here.)
230
+ const _analyzed = new WeakSet<Combinator<unknown>>()
231
+
232
+ export function parse<T>(
233
+ combinator: Combinator<T>,
234
+ input: string,
235
+ opts: ParseOptions = {}
236
+ ): ParseResult<T> {
237
+ if (!_analyzed.has(combinator)) {
238
+ _analyzed.add(combinator)
239
+ markUnusedValues(combinator)
240
+ }
241
+ const trackLines = opts.trackLines ?? combinator._meta.grammarTrackLines ?? false
242
+ const lineContext = trackLines ? createParseLineContext(input, 0) : undefined
243
+ const lineIndex = lineContext?.lineIndex
244
+ const _errors = opts.recover ? [] : undefined
245
+ // In recovery mode also track the furthest-position failure, so the caller can
246
+ // report "where it got stuck + what was expected" even when a permissive top
247
+ // rule succeeds with unconsumed trailing input. Off by default (it adds
248
+ // bookkeeping to every failed alternative).
249
+ const _probe = opts.recover ? { offset: input.length, best: null as ParseFail | null } : undefined
250
+ // Grammar-level ambient trivia declared via rules({ trivia }, factory): install
251
+ // it as ctx.trivia so it's ambient (interpreter). parser/noTrivia override locally.
252
+ const grammarTrivia = combinator._meta.grammarTrivia
253
+ const grammarScanSkip = combinator._meta.grammarScanSkip
254
+ // ONE shape — see `createParseContext`. Stores, not conditional spreads.
255
+ const ctx = createParseContext()
256
+ ctx.trackLines = trackLines
257
+ if (lineIndex) ctx._lineIndex = lineIndex
258
+ if (lineContext) ctx._lineScannedTo = lineContext.lineScannedTo
259
+ if (grammarTrivia !== undefined) {
260
+ ctx.trivia = grammarTrivia
261
+ if (grammarTrivia._meta.triviaKindLabels) ctx.triviaKindLabels = grammarTrivia._meta.triviaKindLabels
262
+ }
263
+ if (grammarScanSkip !== undefined) ctx.scanSkip = grammarScanSkip
264
+ if (_errors !== undefined) ctx._errors = _errors
265
+ if (_probe !== undefined) ctx._probe = _probe
266
+ const result = combinator.parse(input, 0, ctx)
267
+ if (!result.ok) return lineIndex ? annotateResultLines(result, normalizeLineIndex(lineIndex)) : result
268
+ const withErrors = _errors !== undefined
269
+ ? { ...result, errors: _errors, furthestFail: _probe?.best ?? null }
270
+ : result
271
+ return lineIndex ? annotateResultLines(withErrors, normalizeLineIndex(lineIndex)) : withErrors
272
+ }