parseman 0.46.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 (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +9929 -7465
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +9929 -7465
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10865 -8535
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10865 -8535
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +40 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9224 -7203
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9219 -7198
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +311 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +311 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11146 -9593
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11146 -9593
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2898 -1623
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2898 -1623
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11375 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11352 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +446 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +493 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +25 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +322 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +221 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +821 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +2994 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1766 -0
  286. package/src/table/emit.ts +418 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1672 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +459 -0
  295. package/src/table/program.ts +855 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,383 @@
1
+ import { run, type RunOptions, type RunResult, type Runnable } from './functional/run.ts'
2
+ import { GRAMMAR_COVERAGE_DEFINITIONS } from './grammar-metadata.ts'
3
+ export { GRAMMAR_COVERAGE_DEFINITIONS } from './grammar-metadata.ts'
4
+ import type { Combinator, DispatchCase, DispatchMatcherCase, ParseContext, ParseResult } from './types.ts'
5
+ import { choice } from './combinators/choice.ts'
6
+ import { dispatch, endsWith, matches, otherwise, startsWith, when, type DispatchArm, type DispatchStringMatcher } from './combinators/dispatch.ts'
7
+ import { attempt } from './combinators/attempt.ts'
8
+ import { getCoreLiteralValue } from './combinators/choice.ts'
9
+ import { not } from './combinators/not.ts'
10
+ import { peek } from './combinators/peek.ts'
11
+ import { node } from './combinators/node.ts'
12
+ import { parser as grammarParser } from './combinators/grammar.ts'
13
+ import { expect } from './combinators/expect.ts'
14
+ import { gate } from './combinators/gate.ts'
15
+ import { label, field, transform, trivia } from './combinators/map.ts'
16
+ import { many, oneOrMore, optional, sepBy } from './combinators/repeat.ts'
17
+ import { scanTo } from './combinators/scanTo.ts'
18
+ import { sequence } from './combinators/sequence.ts'
19
+ import { token } from './combinators/token.ts'
20
+ import { leaf } from './combinators/token.ts'
21
+ import { withCtx } from './combinators/withCtx.ts'
22
+ import { composedCoverageRules } from './compiler/linker.ts'
23
+ import { buildGrammarPlan, type GrammarCoverageDefinition, type GrammarCoveragePlan } from './compiler/grammar-coverage-ids.ts'
24
+
25
+ export type { GrammarCoverageDefinition } from './compiler/grammar-coverage-ids.ts'
26
+
27
+ function dispatchMatcher(entry: DispatchMatcherCase): DispatchStringMatcher {
28
+ switch (entry.kind) {
29
+ case 'startsWith':
30
+ return startsWith(entry.value)
31
+ case 'endsWith':
32
+ return endsWith(entry.value)
33
+ case 'matches':
34
+ return matches(new RegExp(entry.value, entry.flags))
35
+ }
36
+ }
37
+
38
+
39
+ export type GrammarCoverageSnapshot = {
40
+ definitions: readonly GrammarCoverageDefinition[]
41
+ hits: readonly string[]
42
+ unhit: readonly string[]
43
+ /**
44
+ * Hit fraction, or `NaN` when there was NOTHING TO MEASURE.
45
+ *
46
+ * An empty definition set is not full coverage. This used to report `1` for it, so a
47
+ * grammar whose definitions failed to load — the exact case
48
+ * `'coverage-definitions-unavailable'` exists for — presented as 100% covered, and any
49
+ * consumer gate of the shape `ratio >= threshold` passed on zero evidence. `NaN`
50
+ * compares false against every threshold, so the gate now FAILS CLOSED by default
51
+ * instead of passing by default. Read `measurable` to tell "nothing to measure" from a
52
+ * genuine 0%.
53
+ */
54
+ ratio: number
55
+ /** False when the definition set was empty, i.e. `ratio` is not a measurement. */
56
+ measurable: boolean
57
+ }
58
+
59
+ export type GrammarCoverageCollector = {
60
+ hit(id: string): void
61
+ snapshot(): GrammarCoverageSnapshot
62
+ reset(): void
63
+ }
64
+
65
+ /** Metadata emitted only by a coverage-enabled macro grammar. It is attached to
66
+ * the grammar map, rather than each rule function, so a caller chooses the
67
+ * explicit public start rule before collecting results. */
68
+ export function compiledGrammarCoverageDefinitions(grammar: Record<string, unknown>): readonly GrammarCoverageDefinition[] {
69
+ const definitions = (grammar as Record<symbol, unknown>)[GRAMMAR_COVERAGE_DEFINITIONS]
70
+ // `definitions.length === 0` is checked EXPLICITLY. `[].every(...)` is vacuously true,
71
+ // so an empty array sailed through this guard and was returned as a valid definition
72
+ // set — the one input whose name the error message spells out was the one input it let
73
+ // past, and it went on to report 100% coverage downstream.
74
+ if (!Array.isArray(definitions)
75
+ || definitions.length === 0
76
+ || !definitions.every((definition): definition is GrammarCoverageDefinition => typeof definition === 'object'
77
+ && definition !== null
78
+ && typeof definition.id === 'string'
79
+ && (definition.kind === 'rule' || definition.kind === 'choice-arm' || definition.kind === 'dispatch-arm' || definition.kind === 'label'))) {
80
+ throw new TypeError('grammar has no coverage definitions; enable grammarCoverage for this macro build')
81
+ }
82
+ return definitions
83
+ }
84
+
85
+ export type GrammarTracePhase = 'enter' | 'attempt' | 'selected' | 'success' | 'failure' | 'backtrack' | 'rollback'
86
+ export type GrammarTraceEvent = { id: string; phase: GrammarTracePhase; offset: number; end?: number }
87
+ export type GrammarTraceSnapshot = { events: readonly GrammarTraceEvent[]; truncated: boolean; dropped: number }
88
+ export type GrammarTraceSink = { write(event: GrammarTraceEvent): void; snapshot(): GrammarTraceSnapshot }
89
+ /** Typed context for a coverage-enabled compiled or macro-generated parser. */
90
+ export type GrammarInstrumentationContext = ParseContext & {
91
+ _grammarCoverage?: (id: string) => void
92
+ _grammarTrace?: GrammarTraceSink
93
+ }
94
+
95
+ /** Create the opt-in context consumed by coverage-enabled compiled output. */
96
+ export function createGrammarInstrumentationContext(options: {
97
+ collector?: GrammarCoverageCollector
98
+ trace?: GrammarTraceSink
99
+ trackLines?: boolean
100
+ state?: unknown
101
+ } = {}): GrammarInstrumentationContext {
102
+ return {
103
+ trackLines: options.trackLines ?? false,
104
+ ...(options.state === undefined ? {} : { state: options.state }),
105
+ ...(options.collector === undefined ? {} : { _grammarCoverage: (id: string) => options.collector!.hit(id) }),
106
+ ...(options.trace === undefined ? {} : { _grammarTrace: options.trace }),
107
+ }
108
+ }
109
+
110
+ /** Bounded first-N trace sink. Exceptions and a false callback detach without
111
+ * changing parser results; post-detach events count as dropped. */
112
+ export function createGrammarTraceSink(options: { capacity: number; write?: (event: GrammarTraceEvent) => boolean | void }): GrammarTraceSink {
113
+ if (!Number.isInteger(options.capacity) || options.capacity < 0) throw new TypeError('trace capacity must be a finite non-negative integer')
114
+ const events: GrammarTraceEvent[] = []
115
+ let detached = false
116
+ let truncated = false
117
+ let dropped = 0
118
+ return {
119
+ write(event) {
120
+ if (detached || events.length >= options.capacity) {
121
+ detached = true; truncated = true; dropped++; return
122
+ }
123
+ events.push(Object.freeze({ ...event }))
124
+ try {
125
+ if (options.write?.(event) === false) { detached = true; truncated = true }
126
+ } catch { detached = true; truncated = true }
127
+ },
128
+ snapshot() { return Object.freeze({ events: Object.freeze([...events]), truncated, dropped }) },
129
+ }
130
+ }
131
+
132
+ export function createGrammarCoverageCollector(definitions: readonly GrammarCoverageDefinition[]): GrammarCoverageCollector {
133
+ const ordered = [...definitions].sort((a, b) => a.id.localeCompare(b.id))
134
+ const known = new Set(ordered.map(definition => definition.id))
135
+ const hits = new Set<string>()
136
+ return {
137
+ hit(id) { if (known.has(id)) hits.add(id) },
138
+ snapshot() {
139
+ const hitList = [...hits].sort()
140
+ const unhit = ordered.map(definition => definition.id).filter(id => !hits.has(id))
141
+ // No `ordered.length === 0 ? 1` default. 0/0 is NaN, which is the honest answer and
142
+ // fails every `>=` threshold; reporting 1 asserted total coverage of nothing.
143
+ return {
144
+ definitions: ordered,
145
+ hits: hitList,
146
+ unhit,
147
+ ratio: hitList.length / ordered.length,
148
+ measurable: ordered.length > 0,
149
+ }
150
+ },
151
+ reset() { hits.clear() },
152
+ }
153
+ }
154
+
155
+ export function grammarCoverageDefinitions(entry: Combinator<unknown>, winners?: Record<string, Combinator<unknown>>): readonly GrammarCoverageDefinition[] {
156
+ return buildGrammarPlan(entry, winners).definitions
157
+ }
158
+
159
+ /** Definitions for a runtime `compose()` result, normalized through its final
160
+ * IR winner map. Opaque precompiled pieces intentionally fail rather than
161
+ * reporting source-piece identities as though they were final grammar IDs. */
162
+ export function composedGrammarCoverageDefinitions(grammar: Record<string, unknown>, startRule: string): readonly GrammarCoverageDefinition[] {
163
+ const winners = composedCoverageRules(grammar)
164
+ if (!winners) throw new TypeError('semantic coverage needs re-lowerable composed IR; this composition contains an opaque artifact')
165
+ const entry = winners[startRule]
166
+ if (!entry) throw new TypeError(`semantic coverage start rule ${JSON.stringify(startRule)} is not a final winner`)
167
+ return grammarCoverageDefinitions(entry, winners)
168
+ }
169
+
170
+ type CoverageMaps = Pick<GrammarCoveragePlan, 'choices' | 'dispatches' | 'attempts' | 'labels' | 'rules'>
171
+
172
+ /**
173
+ * Build a separate interpreter graph for the coverage run. It never mutates the
174
+ * source grammar and ordinary Parseman combinators never branch on coverage.
175
+ *
176
+ * Every structural ParserDef is rebuilt from coverage-aware children. Terminal
177
+ * recognizers are safely shared because they contain no child execution.
178
+ */
179
+ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCollector, maps: CoverageMaps, trace?: GrammarTraceSink): Combinator<unknown> {
180
+ const cache = new WeakMap<Combinator<unknown>, Combinator<unknown>>()
181
+ const build = (parser: Combinator<unknown>): Combinator<unknown> => {
182
+ const existing = cache.get(parser)
183
+ if (existing) return existing
184
+ const rule = maps.rules.get(parser)
185
+ const wrap = (parse: (input: string, pos: number, ctx: ParseContext) => ParseResult<unknown>): Combinator<unknown> => {
186
+ const wrapped: Combinator<unknown> = {
187
+ _tag: parser._tag,
188
+ _meta: parser._meta,
189
+ _def: parser._def,
190
+ parse(input, pos, ctx) {
191
+ if (rule) { collector.hit(rule); trace?.write({ id: rule, phase: 'enter', offset: pos }) }
192
+ const result = parse(input, pos, ctx)
193
+ if (rule) trace?.write(result.ok ? { id: rule, phase: 'success', offset: pos, end: result.span.end } : { id: rule, phase: 'failure', offset: result.span.end })
194
+ return result
195
+ },
196
+ }
197
+ cache.set(parser, wrapped)
198
+ return wrapped
199
+ }
200
+ const def = parser._def
201
+ if (def.tag === 'lazy') {
202
+ return wrap((input, pos, ctx) => build(def.thunk()).parse(input, pos, ctx))
203
+ }
204
+ if (def.tag === 'label') {
205
+ const base = label(def.label, build(def.parser))
206
+ return wrap((input, pos, ctx) => {
207
+ const result = base.parse(input, pos, ctx)
208
+ if (result.ok) for (const id of maps.labels.get(parser) ?? []) { collector.hit(id); trace?.write({ id, phase: 'success', offset: pos, end: result.span.end }) }
209
+ return result
210
+ })
211
+ }
212
+ if (def.tag === 'choice') {
213
+ const ids = maps.choices.get(parser) ?? []
214
+ type ChoiceState = { lastSuccessfulArm: number | undefined; successfulArms: number[] }
215
+ const invocations: ChoiceState[] = []
216
+ const arms = def.parsers.map((arm, index) => {
217
+ const child = build(arm)
218
+ return {
219
+ _tag: child._tag,
220
+ _meta: child._meta,
221
+ _def: child._def,
222
+ parse(input: string, pos: number, ctx: ParseContext) {
223
+ const id = ids[index]
224
+ if (id) trace?.write({ id, phase: 'attempt', offset: pos })
225
+ const result = child.parse(input, pos, ctx)
226
+ if (result.ok) {
227
+ // Greedy classification runs its super arm as an implementation
228
+ // detail, then chooses the final semantic arm below. Only that
229
+ // final arm belongs in coverage.
230
+ if (def.strategy.tag !== 'greedyClassify') collector.hit(ids[index]!)
231
+ const invocation = invocations[invocations.length - 1]
232
+ if (invocation) { invocation.lastSuccessfulArm = index; invocation.successfulArms.push(index) }
233
+ }
234
+ else if (id) { trace?.write({ id, phase: 'failure', offset: result.span.end }); trace?.write({ id, phase: 'backtrack', offset: pos }) }
235
+ return result
236
+ },
237
+ } satisfies Combinator<unknown>
238
+ })
239
+ const args = arms.map((arm, index) => def.gates[index] ? { combinator: arm, gate: def.gates[index]! } : arm)
240
+ const unsafeChoice = choice as unknown as (...items: unknown[]) => Combinator<unknown>
241
+ const baseChoice = unsafeChoice(...args)
242
+ return wrap((input, pos, ctx) => {
243
+ const invocation: ChoiceState = { lastSuccessfulArm: undefined, successfulArms: [] }
244
+ invocations.push(invocation)
245
+ const result = baseChoice.parse(input, pos, ctx)
246
+ const strategy = def.strategy
247
+ if (result.ok && strategy.tag === 'greedyClassify') {
248
+ const text = input.slice(pos, result.span.end)
249
+ const classified = def.parsers.findIndex((arm, index) => index !== strategy.superIndex && getCoreLiteralValue(arm) === text)
250
+ if (classified !== -1) invocation.lastSuccessfulArm = classified
251
+ else invocation.lastSuccessfulArm = strategy.superIndex
252
+ }
253
+ if (result.ok && invocation.lastSuccessfulArm !== undefined) {
254
+ const id = ids[invocation.lastSuccessfulArm]
255
+ for (const rejected of invocation.successfulArms) {
256
+ if (rejected !== invocation.lastSuccessfulArm) {
257
+ const rejectedId = ids[rejected]
258
+ if (rejectedId) { trace?.write({ id: rejectedId, phase: 'failure', offset: pos }); trace?.write({ id: rejectedId, phase: 'backtrack', offset: pos }) }
259
+ }
260
+ }
261
+ if (id) {
262
+ collector.hit(id)
263
+ trace?.write({ id, phase: 'selected', offset: pos, end: result.span.end })
264
+ trace?.write({ id, phase: 'success', offset: pos, end: result.span.end })
265
+ }
266
+ }
267
+ invocations.pop()
268
+ return result
269
+ })
270
+ }
271
+ const base: Combinator<unknown> = (() => {
272
+ switch (def.tag) {
273
+ case 'sequence': {
274
+ const unsafeSequence = sequence as unknown as (...items: Combinator<unknown>[]) => Combinator<unknown>
275
+ return unsafeSequence(...def.parsers.map(build))
276
+ }
277
+ case 'dispatch': {
278
+ const ids = maps.dispatches.get(parser) ?? []
279
+ let armIndex = 0
280
+ const dispatchStarts: number[] = []
281
+ const wrapArm = (arm: Combinator<unknown>, id: string | undefined): Combinator<unknown> => {
282
+ const child = build(arm)
283
+ return {
284
+ _tag: child._tag,
285
+ _meta: child._meta,
286
+ _def: child._def,
287
+ parse(input: string, pos: number, ctx: ParseContext) {
288
+ const dispatchStart = dispatchStarts[dispatchStarts.length - 1] ?? pos
289
+ if (id) trace?.write({ id, phase: 'attempt', offset: dispatchStart })
290
+ const result = child.parse(input, pos, ctx)
291
+ if (result.ok) {
292
+ if (id) {
293
+ collector.hit(id)
294
+ trace?.write({ id, phase: 'selected', offset: dispatchStart, end: result.span.end })
295
+ trace?.write({ id, phase: 'success', offset: dispatchStart, end: result.span.end })
296
+ }
297
+ } else if (id) {
298
+ trace?.write({ id, phase: 'failure', offset: result.span.end })
299
+ }
300
+ return result
301
+ },
302
+ } satisfies Combinator<unknown>
303
+ }
304
+ const arms: DispatchArm<unknown>[] = def.cases.map((entry: DispatchCase) =>
305
+ when(entry.keys, wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined)
306
+ )
307
+ if (def.matchers) {
308
+ for (const entry of def.matchers) {
309
+ arms.push(when(dispatchMatcher(entry), wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined))
310
+ }
311
+ }
312
+ if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])))
313
+ const unsafeDispatch = dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
314
+ const base = unsafeDispatch(build(def.selector) as Combinator<string>, ...arms)
315
+ return {
316
+ ...base,
317
+ parse(input, pos, ctx) {
318
+ dispatchStarts.push(pos)
319
+ try {
320
+ return base.parse(input, pos, ctx)
321
+ } finally {
322
+ dispatchStarts.pop()
323
+ }
324
+ },
325
+ }
326
+ }
327
+ case 'many': return many(build(def.parser))
328
+ case 'oneOrMore': return oneOrMore(build(def.parser))
329
+ case 'optional': return optional(build(def.parser))
330
+ case 'attempt': {
331
+ const base = attempt(build(def.parser))
332
+ const id = maps.attempts.get(parser)
333
+ return id === undefined ? base : {
334
+ ...base,
335
+ parse(input, pos, ctx) {
336
+ const result = base.parse(input, pos, ctx)
337
+ if (!result.ok) trace?.write({ id, phase: 'rollback', offset: pos })
338
+ return result
339
+ },
340
+ }
341
+ }
342
+ case 'sepBy': return sepBy(build(def.parser), build(def.separator), { min: def.min, ...(def.max === undefined ? {} : { max: def.max }), ...(def.trailing === undefined ? {} : { trailing: def.trailing }) })
343
+ case 'transform': return transform(build(def.parser), def.fn)
344
+ case 'trivia': return trivia(build(def.parser))
345
+ case 'token': return token(build(def.parser))
346
+ case 'leaf': return leaf(build(def.parser), def.fn)
347
+ case 'field': return field(def.name, build(def.parser))
348
+ case 'grammar': return grammarParser({
349
+ ...(def.triviaParser === undefined ? (def.clearTrivia ? { trivia: null } : {}) : { trivia: build(def.triviaParser) }),
350
+ ...(def.captureTrivia ? { captureTrivia: true } : {}),
351
+ ...(def.trackLines ? { trackLines: true } : {}),
352
+ }, build(def.parser))
353
+ case 'not': return not(build(def.parser))
354
+ case 'peek': return peek(build(def.parser))
355
+ case 'node': {
356
+ const opts = { ...(def.unwrap ? { unwrap: true } : {}), ...(def.collapse ? { collapse: true } : {}), ...(def.project !== undefined ? { project: def.project } : {}), ...(def.captureTrivia ? { captureTrivia: true } : {}), ...(def.trailingTrivia ? { trailingTrivia: true } : {}) }
357
+ return def.type === undefined ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts)
358
+ }
359
+ case 'guard': return gate(def.predicate)
360
+ case 'withCtx': return withCtx(def.extra, build(def.parser))
361
+ case 'expect': return expect(build(def.parser), def.label)
362
+ case 'scanTo': return scanTo(build(def.sentinel), { skip: def.skip.map(build), orEOF: def.orEOF })
363
+ // Zero-width assertion with no coverage-bearing child; reuse it verbatim so
364
+ // the rebuilt sequence still recognises the boundary marker.
365
+ case 'adjacency': return parser
366
+ case 'literal': case 'regex': case 'keywords': return parser
367
+ case 'recover': case 'unknown': throw new TypeError(`runWithGrammarCoverage does not yet support ${def.tag}`)
368
+ default: return parser
369
+ }
370
+ })()
371
+ return wrap((input, pos, ctx) => base.parse(input, pos, ctx))
372
+ }
373
+ return build(entry)
374
+ }
375
+
376
+ export function runWithGrammarCoverage(entry: Runnable, input: string, options: RunOptions & { collector?: GrammarCoverageCollector; trace?: GrammarTraceSink } = {}): { result: RunResult; coverage: GrammarCoverageSnapshot } {
377
+ if (typeof entry === 'function') throw new TypeError('runWithGrammarCoverage currently requires an interpreter combinator entry')
378
+ const plan = buildGrammarPlan(entry)
379
+ const definitions = plan.definitions
380
+ const collector = options.collector ?? createGrammarCoverageCollector(definitions)
381
+ const { collector: _collector, trace, ...runOptions } = options
382
+ return { result: run(coverageEntry(entry, collector, plan, trace), input, runOptions), coverage: collector.snapshot() }
383
+ }
@@ -0,0 +1,297 @@
1
+ import type { ParseContext, Span } from '../types.ts'
2
+ import { annotateSpanFromLineContext } from '../line-index.ts'
3
+
4
+ /** Lazy per-node capture state — arrays materialized on first push. */
5
+ export type CstCaptureBuf = {
6
+ /** Table-host specialisation: collect source-order raw entries without the
7
+ * duplicate semantic children view. Selected once for the enclosing node. */
8
+ rawOnly?: true | undefined
9
+ single?: unknown | undefined
10
+ ch?: unknown[] | undefined
11
+ rawSingle?: unknown | undefined
12
+ raw?: unknown[] | undefined
13
+ tl?: number[] | undefined
14
+ }
15
+
16
+ const EMPTY: unknown[] = []
17
+ const EMPTY_TL: number[] = []
18
+
19
+ export function cstCaptureActive(ctx: ParseContext): boolean {
20
+ return ctx._cstBuf !== undefined || ctx._cstLeaves !== undefined
21
+ }
22
+
23
+ export type CstNodeCaptureSaved = {
24
+ ch: ParseContext['_cstChildren']
25
+ lv: ParseContext['_cstLeaves']
26
+ raw: ParseContext['_cstRawChildren']
27
+ tl: ParseContext['_cstTriviaLog']
28
+ cap: boolean | undefined
29
+ buf: CstCaptureBuf | undefined
30
+ }
31
+
32
+ export function beginCstNodeCapture(ctx: ParseContext): CstNodeCaptureSaved {
33
+ const saved: CstNodeCaptureSaved = {
34
+ ch: ctx._cstChildren,
35
+ lv: ctx._cstLeaves,
36
+ raw: ctx._cstRawChildren,
37
+ tl: ctx._cstTriviaLog,
38
+ cap: ctx.captureTrivia,
39
+ buf: ctx._cstBuf,
40
+ }
41
+ ctx._cstBuf = {}
42
+ ctx._cstChildren = undefined
43
+ ctx._cstLeaves = undefined
44
+ ctx._cstRawChildren = undefined
45
+ ctx._cstTriviaLog = undefined
46
+ ctx.captureTrivia = true
47
+ return saved
48
+ }
49
+
50
+ export function finishCstBuf(buf: CstCaptureBuf | undefined): {
51
+ children: unknown[]
52
+ rawChildren: unknown[]
53
+ triviaLog: number[]
54
+ } {
55
+ if (!buf) return { children: EMPTY, rawChildren: EMPTY, triviaLog: EMPTY_TL }
56
+ const children = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY)
57
+ const rawChildren = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY)
58
+ const triviaLog = buf.tl ?? EMPTY_TL
59
+ return { children, rawChildren, triviaLog }
60
+ }
61
+
62
+ export function endCstNodeCapture(
63
+ ctx: ParseContext,
64
+ saved: CstNodeCaptureSaved,
65
+ ): { children: unknown[]; rawChildren: unknown[]; triviaLog: number[] } {
66
+ const materialized = finishCstBuf(ctx._cstBuf)
67
+ ctx._cstBuf = saved.buf
68
+ ctx._cstChildren = saved.ch
69
+ ctx._cstLeaves = saved.lv
70
+ ctx._cstRawChildren = saved.raw
71
+ ctx._cstTriviaLog = saved.tl
72
+ ctx.captureTrivia = saved.cap
73
+ return materialized
74
+ }
75
+
76
+ export function pushCstLeaf(ctx: ParseContext, leaf: unknown): void {
77
+ if (!ctx.trackLines) {
78
+ pushCstChild(ctx, leaf, leaf)
79
+ return
80
+ }
81
+ const annotated = leaf && typeof leaf === 'object' && (leaf as { span?: unknown }).span
82
+ ? { ...(leaf as Record<string, unknown>), span: annotateSpanFromLineContext((leaf as { span: Span }).span, ctx) }
83
+ : leaf
84
+ pushCstChild(ctx, annotated, annotated)
85
+ }
86
+
87
+ /** Record a built sub-node into the active collector (children vs rawChildren may differ). */
88
+ export function pushCstChild(ctx: ParseContext, built: unknown, rawEntry: unknown): void {
89
+ const b = ctx._cstBuf
90
+ if (b) {
91
+ if (b.rawOnly !== true) {
92
+ if (b.ch) b.ch.push(built)
93
+ else if (b.single !== undefined) { b.ch = [b.single, built]; b.single = undefined }
94
+ else b.single = built
95
+ }
96
+
97
+ if (b.raw) b.raw.push(rawEntry)
98
+ else if (b.rawSingle !== undefined) { b.raw = [b.rawSingle, rawEntry]; b.rawSingle = undefined }
99
+ else b.rawSingle = rawEntry
100
+ return
101
+ }
102
+ if (ctx._cstChildren) ctx._cstChildren.push(built)
103
+ else if (ctx._cstLeaves) ctx._cstLeaves.push(built)
104
+ if (ctx._cstRawChildren) ctx._cstRawChildren.push(rawEntry)
105
+ }
106
+
107
+ export function cstRawLen(ctx: ParseContext): number {
108
+ const b = ctx._cstBuf
109
+ if (b) {
110
+ if (b.raw) return b.raw.length
111
+ return b.rawSingle !== undefined ? 1 : 0
112
+ }
113
+ return ctx._cstRawChildren?.length ?? 0
114
+ }
115
+
116
+ export function cstLeavesLen(ctx: ParseContext): number {
117
+ const b = ctx._cstBuf
118
+ if (b) {
119
+ if (b.ch) return b.ch.length
120
+ return b.single !== undefined ? 1 : 0
121
+ }
122
+ return ctx._cstLeaves?.length ?? 0
123
+ }
124
+
125
+ export function cstTlLen(ctx: ParseContext): number {
126
+ const b = ctx._cstBuf
127
+ if (b) return b.tl?.length ?? 0
128
+ return ctx._cstTriviaLog?.length ?? 0
129
+ }
130
+
131
+ export type CstRollbackMark = { raw: number; tlog: number; leaves: number; fields: number; errors: number }
132
+
133
+ export function saveCstMark(ctx: ParseContext): CstRollbackMark {
134
+ return {
135
+ raw: cstRawLen(ctx),
136
+ tlog: cstTlLen(ctx),
137
+ leaves: cstLeavesLen(ctx),
138
+ fields: ctx._fields?.length ?? 0,
139
+ // Speculative rollback must truncate the flat recovery-error sink in lockstep
140
+ // with the CST leaves: a branch that recovered (pushing a ParseError to
141
+ // ctx._errors AND embedding it as a CST child via captureError) then failed
142
+ // must remove BOTH, or the flat error survives as a GHOST that a tree walk
143
+ // can't see (flat run().errors would disagree with the embedded set).
144
+ errors: ctx._errors?.length ?? 0,
145
+ }
146
+ }
147
+
148
+ function rollbackBufList(
149
+ b: CstCaptureBuf,
150
+ keyMulti: 'ch' | 'raw',
151
+ keySingle: 'single' | 'rawSingle',
152
+ len: number,
153
+ ): void {
154
+ const arr = b[keyMulti]
155
+ if (arr) {
156
+ if (len === 0) b[keyMulti] = undefined
157
+ else if (len === 1) { b[keySingle] = arr[0]; b[keyMulti] = undefined }
158
+ // Guarded like every other truncation — see rollbackCstCapture. This is the
159
+ // buffered node-capture path, so it is reached only by the INTERPRETER (the
160
+ // compiled engine inlines its own capture and never calls here), and it is
161
+ // where the redundant-store rate is highest: over a backtracking fixture
162
+ // this branch ran 32,800 times and 31,198 of those — 95% — restored a
163
+ // length that had not moved.
164
+ else if (arr.length !== len) arr.length = len
165
+ return
166
+ }
167
+ if (len === 0) b[keySingle] = undefined
168
+ }
169
+
170
+ /**
171
+ * Demote everything captured since `childrenLen` from the STRUCTURAL `children`
172
+ * array to `rawChildren` only — it stays in the tree, it stops being a child.
173
+ *
174
+ * This is the one mechanism behind the rule that a list-producing combinator
175
+ * contributes the items of the list and NOTHING ELSE. `sepBy`'s separator is
176
+ * matched by a real combinator, so it pushes through the same nine push sites
177
+ * every terminal does; there is no separate channel to route it into at match
178
+ * time. Demoting after the fact is the cheapest correct answer: it reuses the
179
+ * mark the loop already took for rollback, truncates one array, and leaves
180
+ * `rawChildren` — the source-order record — untouched, so the separator is
181
+ * still recoverable WITHOUT re-reading source bytes.
182
+ *
183
+ * `raw` is deliberately NOT truncated. That asymmetry is the whole point, and it
184
+ * is the same asymmetry trivia already has: consumed, absent from `children`,
185
+ * still reachable.
186
+ */
187
+ export function demoteCapturedToRaw(ctx: ParseContext, childrenLen: number): void {
188
+ const b = ctx._cstBuf
189
+ if (b) {
190
+ rollbackBufList(b, 'ch', 'single', childrenLen)
191
+ return
192
+ }
193
+ // `_cstChildren` and `_cstLeaves` are installed as the SAME array by both the
194
+ // compiled prologue (codegen `_ctx._cstChildren = chV; _ctx._cstLeaves = chV`)
195
+ // and `beginCstNodeCapture`, and `cstLeavesLen`/`rollbackCstCapture` measure and
196
+ // truncate `_cstLeaves` alone. Mirror them exactly — a second truncation here
197
+ // would be a no-op on every real path and a silent divergence on any path where
198
+ // it was not.
199
+ if (ctx._cstLeaves && ctx._cstLeaves.length !== childrenLen) ctx._cstLeaves.length = childrenLen
200
+ }
201
+
202
+ export function rollbackCstCapture(ctx: ParseContext, mark: CstRollbackMark): void {
203
+ rollbackCstCaptureAt(ctx, mark.raw, mark.tlog, mark.leaves, mark.fields, mark.errors)
204
+ }
205
+
206
+ /**
207
+ * The same rollback, taking the five marks as SCALARS.
208
+ *
209
+ * `saveCstMark` allocates a five-field object, and the table driver took one per
210
+ * choice attempt and per repetition item — on `benchmark.less` that is 5.0%
211
+ * (`saveCstMark`) plus 2.1% (`saveTriviaMark`, which allocates this one AND a
212
+ * seven-field wrapper) of the whole parse. The compiled engine holds the same
213
+ * five numbers in scalar locals and allocates nothing, which is the only
214
+ * difference; a caller that keeps them in locals can now do the same.
215
+ *
216
+ * `rollbackCstCapture` stays as the object-taking entry point because the
217
+ * interpreter's call sites genuinely carry a mark around.
218
+ */
219
+ export function rollbackCstCaptureAt(
220
+ ctx: ParseContext,
221
+ raw: number,
222
+ tlog: number,
223
+ leaves: number,
224
+ fields: number,
225
+ errors: number,
226
+ ): void {
227
+ // Truncate the flat recovery-error sink alongside the CST, so a rolled-back
228
+ // speculative branch leaves no ghost error (see saveCstMark). Guarded on a
229
+ // present sink + a defined mark (rollbackTrivia forwards the same field).
230
+ //
231
+ // Each truncation is guarded on `length !== mark`, mirroring the compiled
232
+ // engine (codegen.ts `captureRestoreBody`). Assigning `array.length` runs V8's
233
+ // length setter — a backing-store trim decision — and costs the same when the
234
+ // value is unchanged, which is the overwhelmingly common case here: a
235
+ // speculative branch that captured nothing still pays for every store. The
236
+ // compare is an in-object load. Both engines must carry the guard or the
237
+ // interpreted path silently keeps the cost the compiled path shed.
238
+ if (ctx._errors && errors !== undefined && ctx._errors.length !== errors) ctx._errors.length = errors
239
+ const b = ctx._cstBuf
240
+ if (b) {
241
+ rollbackBufList(b, 'raw', 'rawSingle', raw)
242
+ rollbackBufList(b, 'ch', 'single', leaves)
243
+ if (b.tl) {
244
+ if (tlog === 0) b.tl = undefined
245
+ else if (b.tl.length !== tlog) b.tl.length = tlog
246
+ }
247
+ if (ctx._fields && ctx._fields.length !== fields) ctx._fields.length = fields
248
+ return
249
+ }
250
+ if (ctx._cstRawChildren && ctx._cstRawChildren.length !== raw) ctx._cstRawChildren.length = raw
251
+ if (ctx._cstTriviaLog && ctx._cstTriviaLog.length !== tlog) ctx._cstTriviaLog.length = tlog
252
+ if (ctx._cstLeaves && ctx._cstLeaves.length !== leaves) ctx._cstLeaves.length = leaves
253
+ if (ctx._fields && ctx._fields.length !== fields) ctx._fields.length = fields
254
+ }
255
+
256
+ export function pushCstTriviaEntry(
257
+ ctx: ParseContext,
258
+ start: number,
259
+ end: number,
260
+ kindIndex?: number,
261
+ ): void {
262
+ const insertIdx = cstRawLen(ctx)
263
+ const b = ctx._cstBuf
264
+ const withKind = ctx.triviaKindLabels !== undefined && kindIndex !== undefined
265
+ if (b) {
266
+ if (!b.tl) {
267
+ b.tl = withKind ? [start, end, insertIdx, kindIndex] : [start, end, insertIdx]
268
+ } else if (withKind) {
269
+ b.tl.push(start, end, insertIdx, kindIndex)
270
+ } else {
271
+ b.tl.push(start, end, insertIdx)
272
+ }
273
+ return
274
+ }
275
+ if (ctx._cstTriviaLog) {
276
+ if (withKind) ctx._cstTriviaLog.push(start, end, insertIdx, kindIndex)
277
+ else ctx._cstTriviaLog.push(start, end, insertIdx)
278
+ }
279
+ }
280
+
281
+ export function pushTriviaLogEntry(
282
+ ctx: ParseContext,
283
+ start: number,
284
+ end: number,
285
+ kindIndex?: number,
286
+ ): void {
287
+ if (!ctx._triviaLog) return
288
+ if (ctx.triviaKindLabels !== undefined && kindIndex !== undefined) {
289
+ ctx._triviaLog.push(start, end, kindIndex)
290
+ } else {
291
+ ctx._triviaLog.push(start, end)
292
+ }
293
+ }
294
+
295
+ export function cstDeferredTriviaActive(ctx: ParseContext): boolean {
296
+ return ctx._triviaLog !== undefined || ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined
297
+ }