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,959 @@
1
+ /**
2
+ * Incremental parse document — incremental re-parse over a rules() registry.
3
+ */
4
+ import type { Combinator, ParseContext, ParseError, ParseResult, ParserDef, Span } from '../types.ts'
5
+ import type { NodeLike, CSTLeaf, CSTError } from '../cst/types.ts'
6
+ import { relativizeCST, absoluteSpanCST } from '../cst/relative-spans.ts'
7
+ import { REC } from '../recovery/scan.ts'
8
+ import { createParseContext } from '../parse-context.ts'
9
+ import { fusedHostModeOf, fusedHostElidedOf } from '../compiler/linker.ts'
10
+ import { assertHostModeCompatible } from '../cst/host-mode.ts'
11
+
12
+ /**
13
+ * Build the parse ctx for a (re)parse. In `tolerant` mode the same recovery bundle
14
+ * the interpreter/`run` use is installed (`_tolerant` + `_rec`), so a broken edit
15
+ * keeps producing a tree (with recovered `ParseError`s embedded) instead of
16
+ * collapsing to `null`. Strict mode is byte-identical to before. Every reparse site
17
+ * — root, localized rule reparse, list-splice middle, and the soundness probes —
18
+ * threads the SAME flag, so a probe's tolerance matches the parse it is checking
19
+ * (a mismatch would spuriously diverge and forgo reuse, never miscompare).
20
+ */
21
+ function mkCtx(rawState: unknown, build: ParseContext['build'], tolerant: boolean): ParseContext {
22
+ // A CST node stores `state ?? null`, so "unset" comes back as `null`. Feeding that
23
+ // null straight back in makes a REPARSE diverge from a fresh parse: the state-clone
24
+ // guard tests `!== undefined`, and `Object.assign({}, null)` is `{}` — so a reused
25
+ // subtree carries `state: {}` where a fresh parse carries `state: null`. Normalize the
26
+ // round-trip back to "unset" at this single seeding site.
27
+ const state = rawState ?? undefined
28
+ return tolerant
29
+ ? { trackLines: false, state, build, _tolerant: true, _rec: REC, _errors: [] }
30
+ : { trackLines: false, state, build }
31
+ }
32
+
33
+ /** A single compiled (or interpreted) rule: parse from `pos`, producing node `N`. */
34
+ export type RuleFn<N> = (input: string, pos: number, ctx: ParseContext) => ParseResult<N>
35
+
36
+ /**
37
+ * Rule name → parser. Each entry is either a bare parse function or a
38
+ * `Combinator` (what `rules()` returns). Passing the `rules()` combinators
39
+ * directly lets `.edit()` inspect the grammar — required for **sound** structural
40
+ * list-reuse (see `structuralReuse`): only a rule the grammar proves is a genuine
41
+ * repetition is ever spliced. A bare-function registry still parses correctly; it
42
+ * just can't be structurally reused (the splice is skipped, never guessed).
43
+ */
44
+ export type Registry<N> = Record<string, RuleFn<N> | Combinator<N>>
45
+
46
+ /** Normalize a registry entry to a callable parse function. */
47
+ function asRuleFn<N>(entry: RuleFn<N> | Combinator<N>): RuleFn<N> {
48
+ return typeof entry === 'function' ? entry : (input, pos, ctx) => entry.parse(input, pos, ctx)
49
+ }
50
+
51
+ /** The combinator def behind a registry entry, if it carries one (bare functions don't). */
52
+ function defOf(entry: unknown): ParserDef | undefined {
53
+ return typeof entry === 'object' && entry !== null && '_def' in entry
54
+ ? (entry as { _def: ParserDef })._def
55
+ : undefined
56
+ }
57
+
58
+ /**
59
+ * What a rule's grammar proves about how it produces its element children.
60
+ * `{ kind: 'sep', text }` — a `sepBy`/`oneOrMoreSep` whose separator is a plain
61
+ * literal, so the exact delimiter bytes are recoverable FROM THE GRAMMAR.
62
+ * `{ kind: 'bare' }` — a `many`/`oneOrMore` with no separator at all.
63
+ */
64
+ type Repetition = { kind: 'sep'; text: string } | { kind: 'bare' }
65
+
66
+ /**
67
+ * The literal TEXT a separator parser is fixed to, or `null` when it isn't fixed.
68
+ * Only a case-sensitive `literal()` (behind transparent wrappers) qualifies: its
69
+ * construction pins the exact bytes. A `choice`, a `regex`, a `keywords`, or a
70
+ * rule reference could have matched more than one thing, so its text is NOT
71
+ * recoverable from the grammar and callers must decline reuse rather than guess.
72
+ */
73
+ function literalTextOf(parser: unknown, depth = 0): string | null {
74
+ const def = defOf(parser)
75
+ if (!def || depth > 24) return null
76
+ const d = def as ParserDef & Record<string, unknown>
77
+ switch (d.tag) {
78
+ case 'literal':
79
+ return d.caseInsensitive ? null : (d.value as string)
80
+ case 'lazy': {
81
+ const thunk = d.thunk as (() => unknown) | undefined
82
+ try { return typeof thunk === 'function' ? literalTextOf(thunk(), depth + 1) : null } catch { return null }
83
+ }
84
+ case 'node':
85
+ case 'transform':
86
+ case 'attempt':
87
+ case 'token':
88
+ case 'leaf':
89
+ case 'label':
90
+ case 'field':
91
+ case 'expect':
92
+ case 'withCtx':
93
+ case 'grammar':
94
+ return literalTextOf(d.parser, depth + 1)
95
+ default:
96
+ return null
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Does this rule's grammar produce its element children via a genuine, unbounded
102
+ * **repetition** (`sepBy` / `many` / `oneOrMore`) — as opposed to a fixed-arity
103
+ * sequence of same-typed tokens (e.g. `Triple = Num ',' Num ',' Num`)? Only the
104
+ * former is sound to structurally reuse: a full reparse accepts any element count,
105
+ * so splicing one in or out matches it; a fixed-arity rule would not. We walk the
106
+ * def, transparently unwrapping semantic wrappers (`node`, `transform`, …) and
107
+ * looking through a top-level `sequence` / `optional` (the `[ (sepBy)? ]` shape),
108
+ * and return the repetition's shape iff one is reachable that way. Structurally
109
+ * indistinguishable-from-the-CST cases (fixed sequences) return `null` and fall
110
+ * back to a full, correct reparse. This is what makes `structuralReuse` sound
111
+ * rather than a promise the caller has to keep.
112
+ *
113
+ * The SEPARATOR comes from here, not from the CST: a `sepBy` contributes only its
114
+ * ITEMS to `children`, so there is no delimiter leaf between two elements to sniff.
115
+ * A combinator may collapse only what its construction makes recoverable — so a
116
+ * `sepBy` whose separator is a literal yields its text, and a `sepBy` whose
117
+ * separator is anything else (or one the author opted out of collapsing with
118
+ * `keepSeparator`) yields `null`: no structural reuse, full correct reparse.
119
+ */
120
+ function producesRepetition(def: ParserDef | undefined, depth = 0): Repetition | null {
121
+ if (!def || depth > 24) return null
122
+ const d = def as ParserDef & Record<string, unknown>
123
+ switch (d.tag) {
124
+ case 'sepBy': {
125
+ if (d.keepSeparators) return null // separators ride `children`; not the items-only shape we splice
126
+ const text = literalTextOf(d.separator)
127
+ return text === null || text.length === 0 ? null : { kind: 'sep', text }
128
+ }
129
+ case 'many':
130
+ case 'oneOrMore':
131
+ return { kind: 'bare' }
132
+ // Rule entries and `ref`s wrap their body in a `lazy` thunk — resolve it. The
133
+ // `depth` cap bounds any self-referential cycle (a repetition, if present, is
134
+ // found shallowly before recursion goes deep).
135
+ case 'lazy': {
136
+ const thunk = d.thunk as (() => { _def?: ParserDef }) | undefined
137
+ let inner: { _def?: ParserDef } | undefined
138
+ try { inner = typeof thunk === 'function' ? thunk() : undefined } catch { return null }
139
+ return producesRepetition(inner?._def, depth + 1)
140
+ }
141
+ // Transparent wrappers — look through to the inner parser.
142
+ case 'node':
143
+ case 'transform':
144
+ case 'attempt':
145
+ case 'token':
146
+ case 'leaf':
147
+ case 'label':
148
+ case 'field':
149
+ case 'expect':
150
+ case 'withCtx':
151
+ case 'grammar':
152
+ case 'optional':
153
+ return producesRepetition(defOf(d.parser), depth + 1)
154
+ // A bracketed/anchored list is a sequence whose element run is a repetition.
155
+ case 'sequence': {
156
+ for (const p of d.parsers as unknown[]) {
157
+ const rep = producesRepetition(defOf(p), depth + 1)
158
+ if (rep) return rep
159
+ }
160
+ return null
161
+ }
162
+ default:
163
+ return null
164
+ }
165
+ }
166
+
167
+ export type ParseDocOptions<N extends NodeLike> = {
168
+ /** Initial grammar state threaded into ctx.state for the root parse. */
169
+ state?: unknown
170
+ /**
171
+ * Reconstruct a parent node with one child replaced (used when grafting a
172
+ * re-parsed subtree into its ancestors). Defaults to a shallow spread, which
173
+ * works for plain-object nodes; class-instance ASTs should supply their own.
174
+ */
175
+ rebuild?: (node: N, children: ReadonlyArray<N | CSTLeaf | CSTError>) => N
176
+ /**
177
+ * Mode host for a linkable/fused grammar (RULE_ABI_PLAN §7): threaded into
178
+ * `ctx.build` on every (re)parse so `node()` rules build a positioned CST /
179
+ * language-service tree instead of their own eval-AST. Unset → the grammar's
180
+ * own builders (eval mode).
181
+ */
182
+ build?: ParseContext['build']
183
+ /**
184
+ * Enable structural list-reuse: on a length-changing *structural* edit (adding
185
+ * or removing a whole element in a collection) that would otherwise force a full
186
+ * reparse, reparse only the disturbed span and reuse the collection's untouched
187
+ * tail elements by identity — turning an insert near the top of a large list from
188
+ * O(list) into O(edit + trailing siblings).
189
+ *
190
+ * OFF by default because it is sound only when a rule whose CST children form a
191
+ * homogeneous, separator-delimited element list is a genuine REPETITION
192
+ * (`many` / `sepBy` / `oneOrMore`), not a fixed-arity sequence of same-typed
193
+ * tokens (e.g. `Triple = Num ',' Num ',' Num`) — the two are structurally
194
+ * indistinguishable without the grammar, and splicing the latter would accept an
195
+ * element count it shouldn't. Turn it on when your list rules are true
196
+ * repetitions (the common case: JSON arrays/objects, CSS value lists, argument
197
+ * lists). Every splice is still guarded (exact tiling + lookahead probe +
198
+ * stateless-tail check) and falls back to a full, correct reparse when unproven;
199
+ * the flag only authorises *attempting* the reuse.
200
+ */
201
+ structuralReuse?: boolean
202
+ /**
203
+ * Parse tolerantly: `many`/`sepBy`/`oneOrMore` recover from a failed element
204
+ * (skip to an inferred sync point, embed a `ParseError` over the skipped span),
205
+ * so a broken edit keeps producing a tree instead of collapsing to `null` — the
206
+ * editor-backend path. Off by default (strict, byte-identical). Recovery is a cold
207
+ * path: well-formed input never triggers it. Every reparse the doc does — root,
208
+ * localized rule, and list-splice — inherits this flag, and the reuse-soundness
209
+ * probes run at the same tolerance so incremental reuse stays valid under recovery
210
+ * (a scan that would cross a splice boundary just falls back to a full reparse).
211
+ */
212
+ tolerant?: boolean
213
+ /**
214
+ * The grammar's trivia rule, used ONLY to compute `unconsumedFrom`: a root rule
215
+ * consumes trivia BETWEEN terms but not after the last, so trailing
216
+ * whitespace/comments would otherwise read as leftover input. Given the trivia
217
+ * rule, the tail is skipped before reporting the first unconsumed offset —
218
+ * matching `run()`'s semantics. Defaults to the root rule's own
219
+ * `_meta.grammarTrivia` (from `rules({ trivia })`); set it only to override.
220
+ */
221
+ trivia?: Combinator<unknown>
222
+ }
223
+
224
+ export interface ParseDoc<N extends NodeLike> {
225
+ /**
226
+ * The parse tree with PARENT-RELATIVE spans — each node's `span` is relative to
227
+ * its parent's start (root base 0). This is the shareable representation: a
228
+ * length-changing `.edit()` keeps every untouched subtree shared by identity,
229
+ * and reading the tree is O(1) (no offset rewrite). For absolute positions use
230
+ * the O(depth) cursor `spanAt(path)`, or `absolutizeCST(doc.tree)` to
231
+ * materialize the whole absolute tree. A fresh non-incremental `node().parse()`
232
+ * result is unchanged — still absolute.
233
+ */
234
+ readonly tree: N | null
235
+ /**
236
+ * Recovery diagnostics collected during the (re)parse — the missing-token
237
+ * `expect()` errors and tolerant-list recovery errors that ride the tree as
238
+ * `parseError` nodes, surfaced here as a flat list too (spans ABSOLUTE). Empty
239
+ * in strict mode. On a hard (non-recovered) parse failure this holds the single
240
+ * top-level failure. This is what makes an editor document able to see syntax
241
+ * errors — a blank `errors: []` (the prior behaviour) hid every recovery.
242
+ */
243
+ readonly errors: ParseError[]
244
+ /**
245
+ * Offset where unparsed input begins — the first non-trivia character the parse
246
+ * left unconsumed (trailing trivia skipped when a trivia rule is available), or
247
+ * `null` if the whole input was consumed. This is how a document detects "the
248
+ * grammar stopped short, there's junk here"; computed exactly as `run()` does.
249
+ */
250
+ readonly unconsumedFrom: number | null
251
+ readonly input: string
252
+ /**
253
+ * Absolute span of the node at `path` (child indices from the root) — O(depth),
254
+ * without materializing the absolute tree. The projection cursor for the
255
+ * relative representation; use it for spot queries on a large incremental doc.
256
+ */
257
+ spanAt(path: readonly number[]): { start: number; end: number }
258
+ /**
259
+ * Incrementally re-parse after a text change. `from`/`to` are byte offsets in
260
+ * the OLD input; `replacement` fills that range (editor change-event shape).
261
+ * Sound: the result tree is always structurally identical to a fresh
262
+ * `parseDoc` of the edited text (the Stage-2 guard falls back to a full
263
+ * reparse whenever reuse can't be proven safe). Reuse/strategy is intentionally
264
+ * NOT reported here — an observer derives it by diffing this tree against the
265
+ * previous one (see the incremental tests); the runtime's job is to be fast,
266
+ * not to measure itself.
267
+ */
268
+ edit(from: number, to: number, replacement: string): ParseDoc<N>
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Tree navigation (generic over NodeLike — no class, no CST assumptions)
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /**
276
+ * A reentry candidate is only worth reparsing if it's meaningfully smaller than
277
+ * the whole document — reparsing a rule that spans (say) >half the input costs
278
+ * about as much as a full reparse with none of the reuse. Above this fraction of
279
+ * the input length, `.edit()` skips straight to a full reparse. Keeps the worst
280
+ * case (a structural edit near the front) at ~1× full reparse, never several.
281
+ */
282
+ const REENTRY_MAX_SPAN_FRACTION = 0.5
283
+
284
+ type FoundNode<N extends NodeLike> = { node: N; path: number[] }
285
+
286
+ function isNode(x: unknown): x is NodeLike {
287
+ return typeof x === 'object' && x !== null && (x as { _tag?: string })._tag === 'node'
288
+ }
289
+
290
+ /**
291
+ * Locate the deepest node containing absolute offset `pos`. The tree stores
292
+ * PARENT-RELATIVE spans (see relative-spans), so we thread each node's absolute
293
+ * start down the descent: a child's absolute start is `nodeAbsStart +
294
+ * child.span.start`.
295
+ */
296
+ function findContaining<N extends NodeLike>(
297
+ node: N,
298
+ nodeAbsStart: number,
299
+ pos: number,
300
+ path: number[] = [],
301
+ ): FoundNode<N> | null {
302
+ for (let i = 0; i < node.children.length; i++) {
303
+ const child = node.children[i]!
304
+ if (!isNode(child)) continue
305
+ const childAbsStart = nodeAbsStart + child.span.start
306
+ const childAbsEnd = nodeAbsStart + child.span.end
307
+ if (childAbsStart <= pos && pos < childAbsEnd) {
308
+ const inner = findContaining(child as N, childAbsStart, pos, [...path, i])
309
+ return inner ?? { node: child as N, path: [...path, i] }
310
+ }
311
+ }
312
+ return null
313
+ }
314
+
315
+ function ancestorsAt<N extends NodeLike>(root: N, path: number[]): N[] {
316
+ const ancestors: N[] = [root]
317
+ let cur: N = root
318
+ for (const idx of path.slice(0, -1)) {
319
+ const child = cur.children[idx]
320
+ if (!child || !isNode(child)) break
321
+ ancestors.push(child as N)
322
+ cur = child as N
323
+ }
324
+ return ancestors
325
+ }
326
+
327
+ function replaceAtPath<N extends NodeLike>(
328
+ rebuild: NonNullable<ParseDocOptions<N>['rebuild']>,
329
+ root: N,
330
+ path: number[],
331
+ newNode: N,
332
+ ): N {
333
+ if (path.length === 0) return newNode
334
+ const [idx, ...rest] = path as [number, ...number[]]
335
+ const newChildren = [...root.children] as Array<N | CSTLeaf | CSTError>
336
+ newChildren[idx] = rest.length === 0
337
+ ? newNode
338
+ : replaceAtPath(rebuild, root.children[idx] as N, rest, newNode)
339
+ return rebuild(root, newChildren)
340
+ }
341
+
342
+ function defaultRebuild<N extends NodeLike>(node: N, children: ReadonlyArray<N | CSTLeaf | CSTError>): N {
343
+ return { ...node, children } as N
344
+ }
345
+
346
+ type SpanChild = { span: { start: number; end: number }; children?: readonly unknown[] }
347
+
348
+ /**
349
+ * Shift a node's PARENT-RELATIVE span by `delta`. Used for siblings that sit
350
+ * *after* a length-changing edit inside the same parent: the node moved as a
351
+ * unit, so both endpoints slide by `delta`, but its own children are relative to
352
+ * *its* start and are unchanged — so `children` is shared by identity. That's the
353
+ * relative-span win: O(1) per trailing sibling, not O(subtree). (Contrast the old
354
+ * absolute model, which had to deep-rewrite every descendant.)
355
+ */
356
+ function shiftRelStart<T>(child: T, delta: number): T {
357
+ const c = child as unknown as SpanChild
358
+ return { ...(c as object), span: { start: c.span.start + delta, end: c.span.end + delta } } as unknown as T
359
+ }
360
+
361
+ /**
362
+ * Graft `newNode` at `path` into a parent-relative tree for a length-changing
363
+ * edit (`delta !== 0`): children before the edit are shared by reference, the
364
+ * edited child is replaced, children *after* it have their relative start slid by
365
+ * `delta` (one shallow alloc each — their subtrees are shared by identity), and
366
+ * each ancestor's relative `span.end` grows by `delta` while its start is
367
+ * untouched. Cost is O(depth + trailing siblings along the spine), independent of
368
+ * how many nodes sit inside those trailing siblings.
369
+ */
370
+ function graftRelative<N extends NodeLike>(root: N, path: number[], newNode: N, delta: number): N {
371
+ if (path.length === 0) return newNode
372
+ const [idx, ...rest] = path as [number, ...number[]]
373
+ const oldChildren = root.children as ReadonlyArray<N | CSTLeaf | CSTError>
374
+ const newChildren = oldChildren.map((child, i) => {
375
+ if (i < idx) return child
376
+ if (i === idx) {
377
+ return rest.length === 0 ? newNode : graftRelative(child as N, rest, newNode, delta)
378
+ }
379
+ return shiftRelStart(child, delta)
380
+ })
381
+ return {
382
+ ...root,
383
+ span: { start: root.span.start, end: root.span.end + delta },
384
+ children: newChildren,
385
+ } as N
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // List-splice reuse (structural edits in a collection)
390
+ //
391
+ // A structural edit — inserting or deleting a whole element in a list — makes the
392
+ // innermost-rule reparse fail to converge, and the containing collection is often
393
+ // too big to reparse (that's the "insert at the top of a large array" case where
394
+ // a whole-rule reparse costs ~a full reparse). Instead of reparsing the whole
395
+ // collection, reparse ONLY the disturbed span between the last untouched element
396
+ // before the edit and the first untouched element after it, then splice: the head
397
+ // children are shared as-is, the freshly-parsed middle replaces the changed run,
398
+ // and the tail children are reused with their relative start slid by `delta`
399
+ // (O(1) each — their subtrees are shared by identity, the relative-span win).
400
+ //
401
+ // Soundness rests on: (1) the reparsed middle exactly fills [reStart, reEnd) and
402
+ // lands on element/separator boundaries; (2) a lookahead probe proving the middle
403
+ // read nothing at/after the splice point (else it peeked into the reused tail);
404
+ // (3) a stateless-reentry precondition on the reused tail (its saved `state` is
405
+ // null), so a forward, context-free grammar reproduces those subtrees unchanged.
406
+ // Any failure ⇒ `null` ⇒ the caller falls back to a full, correct reparse. The
407
+ // incremental oracle fuzz (edit() ≡ full reparse, 400 seeds × 3 list grammars +
408
+ // edge cases) is the end-to-end correctness net.
409
+ // ---------------------------------------------------------------------------
410
+
411
+ type AnyChild = { readonly _tag: string; readonly span: { start: number; end: number }; readonly value?: string; readonly type?: string; readonly state?: unknown; readonly children?: readonly unknown[] }
412
+
413
+ /**
414
+ * Parse the collection's disturbed middle `[reStart, reEnd)`, C-relative, and
415
+ * return ONLY its element children — a `sepBy` contributes items and nothing else
416
+ * to `children`, so the spliced middle must match what a full reparse produces:
417
+ * separators are matched and CONSUMED here, never emitted.
418
+ *
419
+ * The region has a uniform shape. `reStart` is the end of the last reused head
420
+ * child and `reEnd` the start of the first reused tail element, which is always an
421
+ * element. So when the head ended on an element NODE the region is `(sep elem)* sep`,
422
+ * and when it ended on a LEAF (the collection's open delimiter — a `sequence` term,
423
+ * still a child) or at the collection's own start the region is `elem (sep elem)* sep`.
424
+ * Either way it ENDS with a separator, which `startsAtElement` seeds and the final
425
+ * `expectElement` assertion enforces: the region must leave us expecting an element,
426
+ * i.e. a separator was just eaten. That is the junction guarantee — a splice can
427
+ * never fuse the middle's last element onto the tail's first with no delimiter
428
+ * between them, which a full reparse could never produce. (An EMPTY region is
429
+ * accepted exactly when the head already ended on a separator or the open
430
+ * delimiter, i.e. `startsAtElement` — the same invariant, vacuously.)
431
+ *
432
+ * Alternation is STRICT: element/element juxtaposition is rejected. When the
433
+ * grammar is separator-free (`separator === null`, a bare `many`/`oneOrMore`)
434
+ * consecutive elements are allowed and no junction constraint applies. Returns the
435
+ * C-relative element children, or `null` if the run doesn't tile `[reStart, reEnd)`
436
+ * exactly on token boundaries.
437
+ */
438
+ function parseMiddle(
439
+ input: string,
440
+ reStart: number,
441
+ reEnd: number,
442
+ cStart: number,
443
+ ruleFn: RuleFn<NodeLike>,
444
+ separator: string | null,
445
+ startsAtElement: boolean,
446
+ build: ParseContext['build'],
447
+ tolerant: boolean,
448
+ ): AnyChild[] | null {
449
+ const ctx: ParseContext = mkCtx(null, build, tolerant)
450
+ const out: AnyChild[] = []
451
+ let pos = reStart
452
+ let guard = 0
453
+ // For a separator-free repetition every position expects an element.
454
+ let expectElement = separator === null ? true : startsAtElement
455
+ while (pos < reEnd) {
456
+ if (++guard > reEnd - reStart + 2) return null // no-progress backstop
457
+ if (separator !== null && !expectElement) {
458
+ if (pos + separator.length > reEnd || !input.startsWith(separator, pos)) return null
459
+ pos += separator.length
460
+ expectElement = true
461
+ continue
462
+ }
463
+ let r: ParseResult<NodeLike>
464
+ try {
465
+ r = ruleFn(input, pos, ctx)
466
+ } catch {
467
+ return null
468
+ }
469
+ if (!r.ok || r.span.end <= pos || r.span.end > reEnd) return null
470
+ out.push(relativizeCST(r.value as unknown as AnyChild, cStart))
471
+ pos = r.span.end
472
+ if (separator !== null) expectElement = false
473
+ }
474
+ // Exact tiling + the junction guarantee (see above).
475
+ return pos === reEnd && expectElement ? out : null
476
+ }
477
+
478
+ /**
479
+ * Try to reuse the untouched tail of collection `C` (at `path`, absolute start
480
+ * `cStart`) around a length-changing edit, reparsing only the disturbed middle.
481
+ * Returns the new relative root, or `null` to fall back to a full reparse.
482
+ */
483
+ function tryListSplice<N extends NodeLike>(
484
+ root: N,
485
+ path: number[],
486
+ C: N,
487
+ cStart: number,
488
+ newInput: string,
489
+ from: number,
490
+ to: number,
491
+ delta: number,
492
+ rep: Repetition,
493
+ registry: Record<string, RuleFn<N>>,
494
+ build: ParseContext['build'],
495
+ tolerant: boolean,
496
+ ): N | null {
497
+ const kids = C.children as ReadonlyArray<AnyChild>
498
+ if (kids.length === 0) return null
499
+
500
+ // head = maximal prefix ending before the edit. Its last child is either an
501
+ // element node (the region then opens on a separator) or a leaf / nothing (the
502
+ // region then opens on an element) — `parseMiddle` handles both, so no back-off
503
+ // is needed and the head stays as long as the edit allows.
504
+ let h = 0
505
+ while (h < kids.length && cStart + kids[h]!.span.end <= from) h++
506
+ // tail = maximal suffix starting after the edit, advanced so it BEGINS at an
507
+ // element (node) boundary — a self-contained element run to reuse.
508
+ let t = kids.length
509
+ while (t > h && cStart + kids[t - 1]!.span.start >= to) t--
510
+ while (t < kids.length && kids[t]!._tag !== 'node') t++
511
+ if (t >= kids.length) return null // nothing reusable after the edit
512
+
513
+ // Stateless-reentry precondition on the reused tail (see soundness note).
514
+ for (let i = t; i < kids.length; i++) {
515
+ if (kids[i]!._tag === 'node' && kids[i]!.state != null) return null
516
+ }
517
+
518
+ // C must look like a genuine homogeneous COLLECTION — a repetition of one
519
+ // element rule joined by one separator — not a fixed heterogeneous sequence
520
+ // (e.g. `Pair = Key ':' Val`, whose `:` is not a list separator). Require: all
521
+ // element (node) children share a type, and there are ≥2 of them. This rejects
522
+ // fixed sequences (their nodes differ in type, or there's only one) so we never
523
+ // treat them as splice-able lists.
524
+ let elemType: string | undefined
525
+ let elemCount = 0
526
+ for (const k of kids) {
527
+ if (k._tag !== 'node') continue
528
+ elemCount++
529
+ if (elemType === undefined) elemType = k.type
530
+ else if (k.type !== elemType) return null // heterogeneous → not a collection
531
+ }
532
+ if (elemType === undefined || elemCount < 2) return null
533
+ const ruleFn = registry[elemType]
534
+ if (!ruleFn) return null
535
+ // The separator comes from the GRAMMAR, not from `children`: a `sepBy` contributes
536
+ // only its items, so there is no delimiter leaf between two elements to sniff.
537
+ // `producesRepetition` already refused any list whose separator its construction
538
+ // doesn't pin (a choice, a regex, a rule ref), so what arrives here is either a
539
+ // fixed literal or a genuinely separator-free repetition.
540
+ const separator = rep.kind === 'sep' ? rep.text : null
541
+
542
+ // The disturbed middle must be pure elements + separators. If the collection is
543
+ // bracketed, its OPENING / CLOSING delimiter (a leaf whose value isn't the
544
+ // separator, sitting before the first / after the last element) must lie OUTSIDE
545
+ // the edit — otherwise the edit changed the collection's own framing and a
546
+ // whole-rule reparse (not a splice) is required.
547
+ const first = kids[0]!
548
+ const last = kids[kids.length - 1]!
549
+ if (first._tag === 'leaf' && first.value !== separator && from < cStart + first.span.end) return null
550
+ if (last._tag === 'leaf' && last.value !== separator && to > cStart + last.span.start) return null
551
+
552
+ const reStart = h > 0 ? cStart + kids[h - 1]!.span.end : cStart
553
+ const reEnd = cStart + kids[t]!.span.start + delta
554
+ if (reEnd < reStart) return null
555
+ // The region opens on an element unless the head ended on one (then it opens on
556
+ // the separator that joined it to what follows). `parseMiddle` carries this
557
+ // through to the junction guarantee.
558
+ const startsAtElement = !(h > 0 && kids[h - 1]!._tag === 'node')
559
+
560
+ const middle = parseMiddle(newInput, reStart, reEnd, cStart, ruleFn as RuleFn<NodeLike>, separator, startsAtElement, build, tolerant)
561
+ if (!middle) return null
562
+
563
+ // Lookahead guard: the middle must have read nothing at/after `reEnd`, else it
564
+ // peeked into the reused tail. Re-run over an input whose tail is overwritten
565
+ // with a sentinel and require an identical middle. Two sentinels so the real
566
+ // byte at `reEnd` can't accidentally match the probe.
567
+ if (reEnd < newInput.length) {
568
+ for (const sentinel of [' ', '￿']) {
569
+ if (newInput[reEnd] === sentinel) continue
570
+ const probed = newInput.slice(0, reEnd) + sentinel.repeat(newInput.length - reEnd)
571
+ const probe = parseMiddle(probed, reStart, reEnd, cStart, ruleFn as RuleFn<NodeLike>, separator, startsAtElement, build, tolerant)
572
+ if (!probe || probe.length !== middle.length) return null
573
+ for (let i = 0; i < middle.length; i++) if (!structurallyEqual(probe[i], middle[i])) return null
574
+ }
575
+ }
576
+
577
+ const head = kids.slice(0, h)
578
+ const tail = kids.slice(t).map((k) => shiftRelStart(k, delta))
579
+ const newC = {
580
+ ...C,
581
+ span: { start: C.span.start, end: C.span.end + delta },
582
+ children: [...head, ...middle, ...tail],
583
+ } as N
584
+
585
+ // Replace C at `path` and slide C's own trailing siblings / ancestor ends.
586
+ return graftRelative(root, path, newC, delta)
587
+ }
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // Stage-2 soundness guard
591
+ // ---------------------------------------------------------------------------
592
+
593
+ /**
594
+ * Deep structural equality on parse trees: `_tag`, `span`, node `type`, leaf
595
+ * `value`, and children pairwise. This is the oracle relation `.edit()` must
596
+ * preserve against a full reparse; it's also what the Stage-2 guard compares
597
+ * probe results with.
598
+ */
599
+ export function structurallyEqual(a: unknown, b: unknown): boolean {
600
+ if (a === b) return true
601
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false
602
+ const at = (a as { _tag?: string })._tag
603
+ const bt = (b as { _tag?: string })._tag
604
+ if (at !== bt) return false
605
+ const as = (a as { span?: { start: number; end: number } }).span
606
+ const bs = (b as { span?: { start: number; end: number } }).span
607
+ if (as || bs) {
608
+ if (!as || !bs || as.start !== bs.start || as.end !== bs.end) return false
609
+ }
610
+ if (at === 'leaf' || at === 'trivia') {
611
+ return (a as { value: string }).value === (b as { value: string }).value
612
+ }
613
+ if (at === 'parseError') {
614
+ // Embedded recovered error: span already compared above; also compare the
615
+ // expected-token set so two errors at the same span but different expectations
616
+ // are not conflated (the oracle must distinguish them).
617
+ const ae = (a as { expected?: readonly string[] }).expected ?? []
618
+ const be = (b as { expected?: readonly string[] }).expected ?? []
619
+ return ae.length === be.length && ae.every((x, i) => x === be[i])
620
+ }
621
+ if ((a as { type?: string }).type !== (b as { type?: string }).type) return false
622
+ const ac = (a as { children?: readonly unknown[] }).children
623
+ const bc = (b as { children?: readonly unknown[] }).children
624
+ if (ac || bc) {
625
+ if (!ac || !bc || ac.length !== bc.length) return false
626
+ for (let i = 0; i < ac.length; i++) if (!structurallyEqual(ac[i], bc[i])) return false
627
+ }
628
+ return true
629
+ }
630
+
631
+ /**
632
+ * A reused suffix spliced at `boundary` (new-input coords) is sound only if the
633
+ * re-parse of the containing rule did NOT read any input at or after `boundary`
634
+ * — otherwise a lookahead or backtrack peeked across the splice and the reused
635
+ * tail could be wrong. We prove independence by re-running the same rule at the
636
+ * same start on an input whose entire tail from `boundary` is overwritten with a
637
+ * sentinel: if the produced node is byte-for-byte structurally identical,
638
+ * nothing past `boundary` was inspected. Two distinct sentinels are tried so the
639
+ * real char at `boundary` can't accidentally equal the probe. Conservative by
640
+ * construction — any probe difference, failure, or throw ⇒ not safe ⇒ the caller
641
+ * widens toward a full reparse (correctness over reuse fraction).
642
+ */
643
+ function boundaryIsSafe<N extends NodeLike>(
644
+ ruleFn: RuleFn<N>,
645
+ newInput: string,
646
+ start: number,
647
+ boundary: number,
648
+ state: unknown,
649
+ build: ParseContext['build'],
650
+ produced: ParseResult<N>,
651
+ tolerant: boolean,
652
+ ): boolean {
653
+ if (!produced.ok) return false
654
+ if (boundary >= newInput.length) return true // nothing after the node to peek at
655
+ for (const sentinel of [' ', '￿']) {
656
+ if (newInput[boundary] === sentinel) continue
657
+ const probed = newInput.slice(0, boundary) + sentinel.repeat(newInput.length - boundary)
658
+ const ctx: ParseContext = mkCtx(state, build, tolerant)
659
+ let r: ParseResult<N>
660
+ try {
661
+ r = ruleFn(probed, start, ctx)
662
+ } catch {
663
+ return false
664
+ }
665
+ if (!r.ok || r.span.end !== produced.span.end) return false
666
+ if (!structurallyEqual(r.value, produced.value)) return false
667
+ }
668
+ return true
669
+ }
670
+
671
+ // ---------------------------------------------------------------------------
672
+ // Document
673
+ // ---------------------------------------------------------------------------
674
+
675
+ class ParseDocImpl<N extends NodeLike> implements ParseDoc<N> {
676
+ private readonly _registry: Registry<N>
677
+ /** Registry entries normalized to callable parse functions (memoized once). */
678
+ private readonly _fns: Record<string, RuleFn<N>>
679
+ /**
680
+ * Rule names the grammar proves are genuine repetitions — the ONLY splice-safe
681
+ * types — mapped to the repetition shape (and, for a `sepBy`, the separator text
682
+ * its construction pins). A rule whose separator isn't recoverable is absent.
683
+ */
684
+ private readonly _reps: Map<string, Repetition>
685
+ private readonly _rootRule: string
686
+ private readonly _opts: ParseDocOptions<N>
687
+ /**
688
+ * The tree, held in whichever coordinate system it arrived in. A fresh parse
689
+ * arrives ABSOLUTE (`_abs` set); an incremental graft arrives PARENT-RELATIVE
690
+ * (`_rel` set). The public `tree` is always the RELATIVE form (so untouched
691
+ * subtrees stay shared by identity across edits and reads are O(1)); it's
692
+ * materialized from `_abs` once, on demand, and memoized. `undefined` = not yet
693
+ * computed; `null` = failed parse. A fresh parse that's never edited nor
694
+ * tree-read pays no conversion — important for the full-reparse fallback path.
695
+ */
696
+ private _abs: N | null | undefined
697
+ private _rel: N | null | undefined
698
+ readonly errors: ParseError[]
699
+ readonly unconsumedFrom: number | null
700
+ readonly input: string
701
+
702
+ constructor(
703
+ registry: Registry<N>,
704
+ rootRule: string,
705
+ opts: ParseDocOptions<N>,
706
+ trees: { abs?: N | null; rel?: N | null },
707
+ errors: ParseError[],
708
+ unconsumedFrom: number | null,
709
+ input: string,
710
+ ) {
711
+ this._registry = registry
712
+ this._fns = {}
713
+ this._reps = new Map()
714
+ for (const [name, entry] of Object.entries(registry)) {
715
+ this._fns[name] = asRuleFn(entry)
716
+ const rep = producesRepetition(defOf(entry))
717
+ if (rep) this._reps.set(name, rep)
718
+ }
719
+ this._rootRule = rootRule
720
+ this._opts = opts
721
+ this._abs = trees.abs
722
+ this._rel = trees.rel
723
+ this.errors = errors
724
+ this.unconsumedFrom = unconsumedFrom
725
+ this.input = input
726
+ }
727
+
728
+ /**
729
+ * The parse tree with PARENT-RELATIVE spans — a node's `span` is relative to
730
+ * its parent's start (root base 0). This is the shareable representation:
731
+ * untouched subtrees keep the same identity across `.edit()`s, and reading the
732
+ * tree after a length-changing edit is O(1) (no offset rewrite). For absolute
733
+ * positions use the O(depth) cursor `spanAt(path)`, or `absolutizeCST(tree)` to
734
+ * materialize the whole absolute tree. (A fresh non-incremental `node().parse()`
735
+ * result is unchanged — still absolute.)
736
+ */
737
+ get tree(): N | null {
738
+ if (this._rel === undefined) {
739
+ this._rel = this._abs ? (relativizeCST(this._abs as unknown as N & { span: Span }, 0) as unknown as N) : null
740
+ }
741
+ return this._rel
742
+ }
743
+
744
+ spanAt(path: readonly number[]): { start: number; end: number } {
745
+ const rel = this.tree
746
+ if (!rel) throw new Error('spanAt on a failed parse (tree is null)')
747
+ return absoluteSpanCST(rel as unknown as { span: Span; children?: readonly unknown[] }, path)
748
+ }
749
+
750
+ /**
751
+ * Wrap a reused (grafted/spliced) RELATIVE tree, recomputing `errors` and
752
+ * `unconsumedFrom` from it so both stay consistent with the tree across the
753
+ * edit (the flat errors are recovered errors embedded in the reused subtrees;
754
+ * unconsumedFrom re-derives trailing junk over the new input).
755
+ */
756
+ private wrapReuse(newTree: N, newInput: string): ParseDoc<N> {
757
+ const trivia = triviaOf(this._registry[this._rootRule], this._opts)
758
+ const end = (newTree as unknown as { span: { end: number } }).span.end
759
+ const errors = collectEmbeddedErrors(newTree, 0, [])
760
+ const unconsumedFrom = unconsumedAfter(end, newInput, trivia)
761
+ return new ParseDocImpl(this._registry, this._rootRule, this._opts, { rel: newTree }, errors, unconsumedFrom, newInput)
762
+ }
763
+
764
+ edit(from: number, to: number, replacement: string): ParseDoc<N> {
765
+ const newInput = this.input.slice(0, from) + replacement + this.input.slice(to)
766
+ const reparse = () => parseDoc(this._registry, this._rootRule, newInput, this._opts)
767
+
768
+ const root = this.tree
769
+ if (!root) return reparse()
770
+
771
+ const delta = replacement.length - (to - from)
772
+ // When `from` sits on the root's own boundary (e.g. right after a top-level
773
+ // element, before its separator), no child node contains it and
774
+ // `findContaining` returns null — but the root still does. Fall back to the
775
+ // root as the container so the structural splice below still gets a shot
776
+ // (this is exactly the "insert/delete a top-level list element" position).
777
+ const found = findContaining(root, 0, from) ?? { node: root, path: [] as number[] }
778
+
779
+ // Try the innermost containing rule first, then widen outward.
780
+ const ancestors = ancestorsAt(root, found.path)
781
+ const candidates: FoundNode<N>[] = [found]
782
+ const pathCopy = [...found.path]
783
+ for (let i = ancestors.length - 2; i >= 0; i--) {
784
+ pathCopy.pop()
785
+ candidates.push({ node: ancestors[i + 1]!, path: [...pathCopy] })
786
+ }
787
+
788
+ const rebuild = this._opts.rebuild ?? defaultRebuild
789
+ for (const { node, path } of candidates) {
790
+ // Absolute span of this candidate, projected from the relative root (O(depth)).
791
+ const { start: absStart, end: absEnd } = absoluteSpanCST(root as unknown as { span: Span; children?: readonly unknown[] }, path)
792
+ // Reentry only pays off when the re-parsed rule is substantially smaller
793
+ // than the whole document. Candidates widen deepest→root (monotonically
794
+ // growing span), so once one covers most of the input, reparsing it — and
795
+ // every larger ancestor after it — can't beat a full reparse and would just
796
+ // be wasted work before the fallback. Bail to the full reparse NOW. This
797
+ // caps `.edit()` at ~one full reparse in the worst case (e.g. a structural
798
+ // insert near the front) instead of stacking several near-full reparses.
799
+ if (absEnd - absStart > this.input.length * REENTRY_MAX_SPAN_FRACTION) break
800
+ const ruleFn = this._fns[node.type]
801
+ if (!ruleFn) continue
802
+ // The reused subtree must FULLY CONTAIN the edited range (old coords).
803
+ // `findContaining` only locates `from`; if the edit's end `to` spills past
804
+ // this node's end, the edit also changed a sibling/separator after it, and
805
+ // reusing the untouched suffix would be unsound — widen to an ancestor
806
+ // that does span the whole edit (ultimately a full reparse).
807
+ if (!(absStart <= from && to <= absEnd)) continue
808
+ const ctx: ParseContext = mkCtx(node.state, this._opts.build, !!this._opts.tolerant)
809
+ const r = ruleFn(newInput, absStart, ctx)
810
+ if (!r.ok) continue
811
+ if (r.span.end !== absEnd + delta) continue
812
+
813
+ // Stage-2 soundness guard: only reuse the untouched suffix if the re-parse
814
+ // provably read no input past its own end (else a lookahead/backtrack
815
+ // crossed the splice). Widen to the next candidate — ultimately a full
816
+ // reparse — when it can't be proven.
817
+ if (!boundaryIsSafe(ruleFn, newInput, absStart, absEnd + delta, node.state, this._opts.build, r, !!this._opts.tolerant)) {
818
+ continue
819
+ }
820
+
821
+ // The re-parse produced an ABSOLUTE subtree; rebase it to parent-relative
822
+ // for splicing into the relative tree. The parent's absolute start is
823
+ // `absStart - node.span.start` (node.span is already relative to it).
824
+ const parentBase = absStart - node.span.start
825
+ const newRel = relativizeCST(r.value as unknown as N & { span: Span }, parentBase) as unknown as N
826
+
827
+ // delta === 0: relative spans are unchanged, so the spine graft (sharing
828
+ // every untouched sibling by reference) is already correct.
829
+ if (delta === 0) {
830
+ const newTree = replaceAtPath(rebuild, root, path, newRel)
831
+ return this.wrapReuse(newTree, newInput)
832
+ }
833
+ // Length-changing edit: trailing siblings' relative starts slide by `delta`.
834
+ // A custom `rebuild` (possibly a class instance) can't have its span slid
835
+ // safely, so fall back to a full, correct reparse.
836
+ if (this._opts.rebuild) return reparse()
837
+ const newTree = graftRelative(root, path, newRel, delta)
838
+ return this.wrapReuse(newTree, newInput)
839
+ }
840
+
841
+ // No localized rule reparse converged — the edit is structural. Before paying
842
+ // a full reparse, try reusing the untouched tail of a containing collection
843
+ // (add/remove an element in a list). We only ever splice a rule the GRAMMAR
844
+ // proves is a genuine repetition (`this._reps`, from its combinator def) — a
845
+ // fixed-arity same-typed sequence is structurally indistinguishable from a list
846
+ // by its CST alone, so splicing it could accept a wrong element count; it's
847
+ // excluded here and falls back to a full, correct reparse. Innermost containing
848
+ // collection first; a candidate whose disturbed middle doesn't tile cleanly
849
+ // returns null and we widen. Correctness net is the incremental oracle fuzz.
850
+ if (this._opts.structuralReuse && delta !== 0 && !this._opts.rebuild) {
851
+ // Candidates run innermost→ancestor but never include the root itself; the
852
+ // splice-able collection can BE the root (e.g. a top-level `sepBy` list), so
853
+ // consider it last.
854
+ const spliceCandidates: FoundNode<N>[] = [...candidates, { node: root, path: [] }]
855
+ for (const { node, path } of spliceCandidates) {
856
+ const rep = this._reps.get(node.type)
857
+ if (!rep) continue // grammar didn't prove this rule a splice-able repetition
858
+ const { start: cStart, end: cEnd } = absoluteSpanCST(root as unknown as { span: Span; children?: readonly unknown[] }, path)
859
+ if (!(cStart <= from && to <= cEnd)) continue
860
+ const spliced = tryListSplice(root, path, node, cStart, newInput, from, to, delta, rep, this._fns, this._opts.build, !!this._opts.tolerant)
861
+ if (spliced) return this.wrapReuse(spliced, newInput)
862
+ }
863
+ }
864
+
865
+ return reparse()
866
+ }
867
+ }
868
+
869
+ /**
870
+ * The relative (parent-offset) tree backing a doc. Currently identical to the
871
+ * public `doc.tree` (which is relative); kept as a named internal handle for the
872
+ * reuse-metric tests, which assert on the shareable representation explicitly.
873
+ */
874
+ export function relTreeOf<N extends NodeLike>(doc: ParseDoc<N>): N | null {
875
+ return doc.tree
876
+ }
877
+
878
+ /**
879
+ * The grammar trivia rule to skip when computing `unconsumedFrom`: an explicit
880
+ * `trivia` option wins; otherwise the root entry's ambient `grammarTrivia` (the
881
+ * same source `run()` derives it from). A bare-function registry carries no meta,
882
+ * so trailing trivia can't be inferred and the parse must reach the exact end.
883
+ */
884
+ function triviaOf<N extends NodeLike>(
885
+ entry: RuleFn<N> | Combinator<N> | undefined,
886
+ opts: ParseDocOptions<N>,
887
+ ): Combinator<unknown> | undefined {
888
+ if (opts.trivia) return opts.trivia
889
+ // Only an interpreter Combinator carries `_meta`; a bare parse function or a
890
+ // compiled-grammar object (which bakes its ambient trivia into codegen) does
891
+ // not, so there's nothing to derive and trailing trivia isn't skipped.
892
+ if (entry === undefined || typeof entry === 'function') return undefined
893
+ const meta = (entry as { _meta?: { grammarTrivia?: Combinator<unknown> } })._meta
894
+ return meta ? meta.grammarTrivia : undefined
895
+ }
896
+
897
+ /**
898
+ * First unconsumed non-trivia offset after a parse that ended at `end` (trailing
899
+ * trivia skipped when a trivia rule is available), or `null` when the whole input
900
+ * was consumed — byte-for-byte the computation in `run()` (run.ts:117-129).
901
+ */
902
+ function unconsumedAfter(end: number, input: string, trivia: Combinator<unknown> | undefined): number | null {
903
+ let pos = end
904
+ if (trivia && pos < input.length) {
905
+ const t = trivia.parse(input, pos, createParseContext())
906
+ if (t.ok && t.span.end > pos) pos = t.span.end
907
+ }
908
+ return pos < input.length ? pos : null
909
+ }
910
+
911
+ /**
912
+ * Collect embedded `parseError` recovery nodes from a PARENT-RELATIVE tree,
913
+ * projecting each to an ABSOLUTE span (root base 0) — the flat mirror of the
914
+ * errors that ride the tree, recomputed for a reuse-path (grafted/spliced) result
915
+ * so `doc.errors` stays consistent with the tree across incremental edits.
916
+ */
917
+ function collectEmbeddedErrors(node: unknown, base: number, out: ParseError[]): ParseError[] {
918
+ const c = node as { _tag?: string; span?: { start: number; end: number }; expected?: string[]; children?: readonly unknown[] }
919
+ if (!c || typeof c !== 'object' || !c.span) return out
920
+ const start = base + c.span.start
921
+ if (c._tag === 'parseError') {
922
+ out.push({ _tag: 'parseError', span: { start, end: base + c.span.end }, expected: c.expected ?? [] })
923
+ return out
924
+ }
925
+ if (Array.isArray(c.children)) for (const k of c.children) collectEmbeddedErrors(k, start, out)
926
+ return out
927
+ }
928
+
929
+ /**
930
+ * Parse `input` from `rootRule` and wrap the result in a ParseDoc that can
931
+ * be incrementally re-parsed via `.edit()`.
932
+ */
933
+ export function parseDoc<N extends NodeLike>(
934
+ registry: Registry<N>,
935
+ rootRule: string,
936
+ input: string,
937
+ opts: ParseDocOptions<N> = {},
938
+ ): ParseDoc<N> {
939
+ const entry = registry[rootRule]
940
+ if (!entry) throw new Error(`No rule '${rootRule}' in registry`)
941
+ // Once per document, not once per node: the fused artifact records the host mode it
942
+ // was lowered for, so a mismatched driver is an error here rather than a wrong-shaped
943
+ // tree later. Every reparse this doc performs reuses the same registry and host.
944
+ assertHostModeCompatible(fusedHostModeOf(registry), opts.build, fusedHostElidedOf(registry))
945
+ const ctx: ParseContext = mkCtx(opts.state, opts.build, !!opts.tolerant)
946
+ const r: ParseResult<N> = asRuleFn(entry)(input, 0, ctx)
947
+ if (r.ok) {
948
+ // A fresh parse is ABSOLUTE; the relative form is materialized lazily on the
949
+ // first edit/spanAt, so a parse that's never edited pays no conversion. The
950
+ // recovery errors the tolerant parse collected into ctx._errors are surfaced
951
+ // flat too (like run()), and unconsumedFrom reports any trailing junk.
952
+ const errors = ctx._errors ? [...ctx._errors] : []
953
+ const unconsumedFrom = unconsumedAfter(r.span.end, input, triviaOf(entry, opts))
954
+ return new ParseDocImpl(registry, rootRule, opts, { abs: r.value }, errors, unconsumedFrom, input)
955
+ }
956
+ // Hard (non-recovered) failure: the single top-level failure, as a parseError.
957
+ const fail: ParseError = { _tag: 'parseError', span: r.span, expected: r.expected }
958
+ return new ParseDocImpl(registry, rootRule, opts, { abs: null }, [fail], null, input)
959
+ }