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,615 @@
1
+ /**
2
+ * Serialize a compiled rule map BACK to a compact combinator-construction source
3
+ * expression — the "IR" a composed grammar carries instead of ~1MB of fully
4
+ * lowered `_r_<Name>` function source. At fuse time the expression is evaluated
5
+ * (with the combinator constructors in scope) to reconstruct the rule map, then
6
+ * re-lowered via `compileLinkable` — identical behaviour, a fraction of the bytes.
7
+ *
8
+ * The output mirrors how a grammar is WRITTEN, not how it lowers:
9
+ * - a rule reference (`g.Foo`, a `lazy` carrying `_ruleName`) → `g["Foo"]`
10
+ * - a sub-combinator shared by identity (referenced ≥2×, e.g. a `balanced` const
11
+ * reused across rules) → a hoisted `const _s<N> = …`, referenced by name, so it
12
+ * stays shared through the round trip (and `compileLinkable` re-hoists it)
13
+ * - a self-referential combinator (`balanced`'s internal `self = ref()`) → a
14
+ * `ref()` + `.define()` closure, the same shape the library combinator emits
15
+ * - `transform`/`node` callbacks → their captured source (`fnSrc`/`buildSrc`)
16
+ *
17
+ * Returns null if the map can't be faithfully serialized (a callback without
18
+ * source, or an unsupported construct) — the caller then keeps the lowered source.
19
+ */
20
+ import type { Combinator, DispatchMatcherCase, ParserDef } from '../types.ts'
21
+ import { rules } from '../combinators/parser.ts'
22
+ import { ref } from '../combinators/ref.ts'
23
+ import { regex } from '../combinators/regex.ts'
24
+ import { literal } from '../combinators/literal.ts'
25
+ import { keywords } from '../combinators/keywords.ts'
26
+ import { sequence } from '../combinators/sequence.ts'
27
+ import { choice } from '../combinators/choice.ts'
28
+ import { dispatch, endsWith, matches, otherwise, routed, startsWith, when } from '../combinators/dispatch.ts'
29
+ import { attempt } from '../combinators/attempt.ts'
30
+ import { many, oneOrMore, optional, sepBy, keepSeparator } from '../combinators/repeat.ts'
31
+ import { not } from '../combinators/not.ts'
32
+ import { peek } from '../combinators/peek.ts'
33
+ import { node } from '../combinators/node.ts'
34
+ import { parser } from '../combinators/grammar.ts'
35
+ import { scanTo, balanced, type BalancedAmbient } from '../combinators/scanTo.ts'
36
+ import { token, leaf } from '../combinators/token.ts'
37
+ import { classifiedTrivia, transform, trivia, label, field } from '../combinators/map.ts'
38
+ import { analyzeLabeledTrivia } from '../cst/trivia-kinds.ts'
39
+ import { expect as expectC } from '../combinators/expect.ts'
40
+ import { withCtx } from '../combinators/withCtx.ts'
41
+ import { adjacent, notAdjacent } from '../combinators/adjacency.ts'
42
+
43
+ type Comb = Combinator<unknown>
44
+
45
+ /** Child combinators of a def, in construction order (mirrors codegen's childrenOf). */
46
+ function childrenOf(def: ParserDef): Comb[] {
47
+ switch (def.tag) {
48
+ case 'sequence':
49
+ case 'choice': return def.parsers
50
+ case 'dispatch': return [def.selector, ...def.cases.map(entry => entry.parser), ...(def.matchers ? def.matchers.map(entry => entry.parser) : []), ...(def.otherwise ? [def.otherwise] : [])]
51
+ case 'many':
52
+ case 'oneOrMore':
53
+ case 'optional':
54
+ case 'attempt':
55
+ case 'transform':
56
+ case 'trivia':
57
+ case 'token':
58
+ case 'leaf':
59
+ case 'label':
60
+ case 'field':
61
+ case 'not':
62
+ case 'peek':
63
+ case 'node':
64
+ case 'withCtx':
65
+ case 'expect': return [def.parser]
66
+ case 'grammar': return def.triviaParser ? [def.parser, def.triviaParser] : [def.parser]
67
+ case 'sepBy': return [def.parser, def.separator]
68
+ case 'scanTo': return [def.sentinel, ...def.skip]
69
+ case 'routed': return def.fallback ? [def.fallback] : []
70
+ case 'lazy':
71
+ case 'literal':
72
+ case 'regex':
73
+ case 'keywords':
74
+ case 'guard':
75
+ case 'adjacency':
76
+ case 'recover':
77
+ case 'unknown': return []
78
+ }
79
+ }
80
+
81
+ const ruleNameOf = (c: Comb): string | undefined =>
82
+ (c as unknown as { _ruleName?: string })._ruleName
83
+
84
+ /**
85
+ * A `balanced()` that must re-resolve ambient `scanSkip` into its interior.
86
+ *
87
+ * The obligation is recorded as an OWN PROPERTY (`_balancedAmbient`), deliberately
88
+ * outside `_def`, so every static analysis keeps seeing the eager interior. Structural
89
+ * serialization therefore loses it: the round trip emits the raw interior, the rebuilt
90
+ * object is an ordinary `transform`, and codegen's ambient-rebuild branch never fires.
91
+ * The composed parser then stops at a delimiter hidden inside a string or comment,
92
+ * while the interpreter and a direct compile of the same grammar do not.
93
+ *
94
+ * So a balanced round-trips as the CONSTRUCTOR CALL that built it, not as its lowered
95
+ * shape — the marker is re-created by `balanced()` itself. Its interior is derived, so
96
+ * the serializer treats it as an atom: it is not descended for sharing/recursion
97
+ * analysis (the interior's `self` ref is internal and must not surface as a cycle).
98
+ */
99
+ const balancedOf = (c: Comb): { open: string; close: string; ownSkip: Comb[] } | undefined =>
100
+ (c as BalancedAmbient)._balancedAmbient
101
+
102
+ /** Resolve a lazy's target, or null if it isn't defined yet (external ref). */
103
+ function lazyTarget(c: Comb): Comb | null {
104
+ if (c._def.tag !== 'lazy') return null
105
+ try { return c._def.thunk() } catch { return null }
106
+ }
107
+
108
+ class Unserializable extends Error {}
109
+
110
+ /** `, { min, max, trailing }` — omitted entirely when everything is at its default. */
111
+ function repeatOpts(min: number, max: number | undefined, trailing?: string): string {
112
+ const parts: string[] = []
113
+ if (min !== 0) parts.push(`min: ${min}`)
114
+ if (max !== undefined) parts.push(`max: ${max}`)
115
+ if (trailing !== undefined) parts.push(`trailing: ${JSON.stringify(trailing)}`)
116
+ return parts.length === 0 ? '' : `, { ${parts.join(', ')} }`
117
+ }
118
+
119
+ function projectOpt(project: Extract<ParserDef, { tag: 'node' }>['project']): string | undefined {
120
+ if (project === undefined) return undefined
121
+ return `project: ${project}`
122
+ }
123
+
124
+ function tagsOpt(tags: Extract<ParserDef, { tag: 'node' }>['tags']): string | undefined {
125
+ if (tags === undefined || tags.length === 0) return undefined
126
+ return `tags: ${JSON.stringify(tags)}`
127
+ }
128
+
129
+ function matcherExpr(entry: DispatchMatcherCase): string {
130
+ switch (entry.kind) {
131
+ case 'startsWith':
132
+ return `startsWith(${JSON.stringify(entry.value)})`
133
+ case 'endsWith':
134
+ return `endsWith(${JSON.stringify(entry.value)})`
135
+ case 'matches':
136
+ return `matches(${new RegExp(entry.value, entry.flags ?? '').toString()})`
137
+ }
138
+ }
139
+
140
+ export function serializeRuleMap(
141
+ ruleMap: ReadonlyArray<readonly [string, Comb]>,
142
+ scanSkip?: readonly Comb[] | null,
143
+ ): string | null {
144
+ try {
145
+ return new Serializer(ruleMap, scanSkip ?? []).run()
146
+ } catch (e) {
147
+ if (e instanceof Unserializable) {
148
+ if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${(e as Error).message}`)
149
+ return null
150
+ }
151
+ throw e
152
+ }
153
+ }
154
+
155
+ /** Reconstruct a rule map from serialized IR (the inverse of `serializeRuleMap`) —
156
+ * evaluate the combinator-construction expression with every constructor in scope.
157
+ * Used at fuse time (runtime linker + build-time plugin) to re-lower carried IR. */
158
+ export function evalRuleMapIR(ir: string): Array<[string, Comb]> {
159
+ // `_tf`/`_nd` reconstruct a transform/node AND restore its captured callback
160
+ // source (`_def.fnSrc`/`buildSrc`) so re-lowering inlines it statically. The live
161
+ // fn is only needed for interpreted mode; a self-contained transform source is
162
+ // eval'd, a node build (which may reference imported AST classes) is left to its
163
+ // source only. Macro-only validation metadata is carried as plain data: this
164
+ // runtime module never parses callback source or imports a compiler frontend.
165
+ const _tf = (child: Comb, src: string, recognitionOnly = false): Comb => {
166
+ let fn: (...a: unknown[]) => unknown
167
+ // eslint-disable-next-line no-eval
168
+ try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR transform fn not materialized') } }
169
+ const t = transform(child as never, fn as never)
170
+ ;(t._def as { fnSrc?: string }).fnSrc = src
171
+ if (recognitionOnly) (t._def as { recognitionOnly?: boolean }).recognitionOnly = true
172
+ return t as Comb
173
+ }
174
+ const _lf = (child: Comb, src: string): Comb => {
175
+ let fn: (...a: unknown[]) => unknown
176
+ // eslint-disable-next-line no-eval
177
+ try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR leaf fn not materialized') } }
178
+ const l = leaf(child as never, fn as never)
179
+ ;(l._def as { fnSrc?: string }).fnSrc = src
180
+ return l as Comb
181
+ }
182
+ const _nd = (type: string, child: Comb, src: string, opts?: unknown, staticError?: readonly string[], sigSrc?: string): Comb => {
183
+ if (staticError !== undefined && staticError.length > 0) {
184
+ throw new Error(`IR direct node builder for ${type} must be macro-static and self-contained; unsupported binding(s): ${staticError.join(', ')}`)
185
+ }
186
+ // A serialized direct builder needs an inert sentinel as well as buildSrc.
187
+ // `node(..., undefined)` is structural, so re-lowering a composed artifact
188
+ // silently routes it through ctx.build/default CST even though the IR still
189
+ // carries the callback source. The compiler is the only consumer that may
190
+ // materialize `buildSrc`; raw IR interpretation deliberately rejects direct
191
+ // builders rather than evaluating arbitrary captured source at runtime.
192
+ const n = node(type, child as never, (() => { throw new Error('IR node build requires static re-lowering') }) as never, opts as never)
193
+ ;(n._def as { buildSrc?: string; buildStaticError?: readonly string[] }).buildSrc = src
194
+ if (staticError !== undefined) (n._def as { buildStaticError?: readonly string[] }).buildStaticError = staticError
195
+ // Analysis-only resolved reducer signature (see `buildAnalysisSrc`). Without it a
196
+ // re-lowered composed artifact silently re-acquires the fail-open capture cost the
197
+ // authoring module had already resolved away.
198
+ if (sigSrc !== undefined) (n._def as { buildSigSrc?: string }).buildSigSrc = sigSrc
199
+ return n as Comb
200
+ }
201
+ // `_gch` rebuilds a GATED choice AND restores its `_def.gateSrcs` (parallel to the
202
+ // arms) — the gate mirror of `_tf`/`_nd`. Each item is either a plain arm or a
203
+ // `[gateSource, arm]` tuple: the source is eval'd to the live predicate (interpreted
204
+ // mode) and recorded as the arm's gateSrcs entry so re-lowering INLINES the gate
205
+ // statically (keeping the artifact fusible via `emitFusedSource`). A missing/failed
206
+ // source falls back to a throwing predicate — the same contract as `_tf`.
207
+ const _gch = (items: Array<Comb | [string, Comb]>): Comb => {
208
+ const gateSrcs: (string | null)[] = []
209
+ const arms = items.map(it => {
210
+ if (Array.isArray(it)) {
211
+ const [src, comb] = it
212
+ gateSrcs.push(src)
213
+ let gate: (s: unknown) => boolean
214
+ // eslint-disable-next-line no-eval
215
+ try { gate = (0, eval)(`(${src})`) } catch { gate = () => { throw new Error('IR gate fn not materialized') } }
216
+ return { gate, combinator: comb }
217
+ }
218
+ gateSrcs.push(null)
219
+ return it
220
+ })
221
+ const c = (choice as (...a: unknown[]) => Comb)(...arms)
222
+ // `choice` is the authority on gate alignment; `gateSrcs` must line up 1:1 with
223
+ // the constructed `_def.gates`. They always do on the normal path (both from the
224
+ // same `items.map`), but assert it so a future change in `choice` that coalesces
225
+ // or drops arms can't silently ship a mis-aligned gate-source array.
226
+ const gates = (c._def as { gates?: unknown[] }).gates
227
+ if (gates && gates.length !== gateSrcs.length) {
228
+ throw new Error(`_gch: gateSrcs length ${gateSrcs.length} != choice gates length ${gates.length}`)
229
+ }
230
+ ;(c._def as { gateSrcs?: (string | null)[] }).gateSrcs = gateSrcs
231
+ return c
232
+ }
233
+ // `_wc` rebuilds a `withCtx` AND restores its `_def.extraSrc` (the source of the
234
+ // `extra`/state value) — the withCtx mirror of `_tf`/`_nd`/`_gch`. The source is
235
+ // eval'd to the live `extra` value (interpreted mode) and recorded on the def so
236
+ // re-lowering INLINES the state getter statically (`() => (extraSrc)`), keeping the
237
+ // artifact fusible via `emitFusedSource`. A plain `withCtx(value, inner)` would
238
+ // leave `extraSrc` unset → codegen emits a source-less runtime closure (a non-static
239
+ // callback) → `emitFusedSource` fails and a downstream `compose()` silently falls
240
+ // back to a runtime fuse. A missing/failed source falls back to `undefined` state —
241
+ // the same best-effort contract as `_tf`.
242
+ const _wc = (src: string, inner: Comb): Comb => {
243
+ let extra: unknown
244
+ // eslint-disable-next-line no-eval
245
+ try { extra = (0, eval)(`(${src})`) } catch { extra = undefined }
246
+ const w = (withCtx as (e: unknown, c: Comb) => Comb)(extra, inner)
247
+ ;(w._def as { extraSrc?: string }).extraSrc = src
248
+ return w
249
+ }
250
+ // eslint-disable-next-line no-new-func
251
+ const fn = new Function(
252
+ 'rules', 'ref', 'regex', 'literal', 'keywords', 'sequence', 'choice', 'dispatch', 'when', 'startsWith', 'endsWith', 'matches', 'otherwise', 'routed', 'attempt',
253
+ 'many', 'oneOrMore', 'optional', 'sepBy', 'keepSeparator', 'not', 'peek', 'node', 'parser',
254
+ 'scanTo', 'balanced', 'token', 'leaf', 'transform', 'trivia', 'classifiedTrivia', 'label', 'field', 'expect', 'adjacent', 'notAdjacent', '_tf', '_lf', '_nd', '_gch', '_wc',
255
+ `return (${ir})`,
256
+ )
257
+ const map = fn(
258
+ rules, ref, regex, literal, keywords, sequence, choice, dispatch, when, startsWith, endsWith, matches, otherwise, routed, attempt,
259
+ many, oneOrMore, optional, sepBy, keepSeparator, not, peek, node, parser,
260
+ scanTo, balanced, token, leaf, transform, trivia, classifiedTrivia, label, field, expectC, adjacent, notAdjacent, _tf, _lf, _nd, _gch, _wc,
261
+ ) as Record<string, Comb>
262
+ return Object.entries(map)
263
+ }
264
+
265
+ /** A recursive combinator currently being emitted, and the local `ref()` var that
266
+ * stands in for its back-edge. Frames NEST: an inline recursive combinator inside
267
+ * another one must still be able to name the outer one's ref var. */
268
+ type SelfFrame = { comb: Comb; var: string }
269
+
270
+ class Serializer {
271
+ private counts = new Map<Comb, number>()
272
+ private selfRef = new Set<Comb>() // combinators reached by an unnamed lazy pointing back into their own subtree
273
+ private cycleInterior = new Set<Comb>() // combinators lying ON a self-reference cycle (strictly below its target)
274
+ private constName = new Map<Comb, string>()
275
+ private emitted = new Set<string>()
276
+ private decls: string[] = []
277
+ private ruleValues = new Set<Comb>()
278
+ private localRuleNames = new Map<Comb, string>()
279
+ private ruleMap: ReadonlyArray<readonly [string, Comb]>
280
+ private scanSkip: readonly Comb[]
281
+
282
+ constructor(ruleMap: ReadonlyArray<readonly [string, Comb]>, scanSkip: readonly Comb[] = []) {
283
+ this.ruleMap = ruleMap
284
+ this.scanSkip = scanSkip
285
+ for (const [name, c] of ruleMap) {
286
+ this.localRuleNames.set(c, name)
287
+ this.ruleValues.add(this.body(c))
288
+ }
289
+ }
290
+
291
+ /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
292
+ * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
293
+ * serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
294
+ * outside the `(g) => …` factory where `g` is bound. A unit that does reference a
295
+ * rule can't be carried in options; bail to the full-pieces fallback. */
296
+ private assertNoRuleRef(c: Comb, seen: Set<Comb>): void {
297
+ if (seen.has(c)) return
298
+ seen.add(c)
299
+ if (c._def.tag === 'lazy') {
300
+ if (ruleNameOf(c) !== undefined) throw new Unserializable('scanSkip unit references a grammar rule')
301
+ const target = lazyTarget(c)
302
+ if (target) this.assertNoRuleRef(target, seen)
303
+ return
304
+ }
305
+ for (const child of childrenOf(c._def)) this.assertNoRuleRef(child, seen)
306
+ }
307
+
308
+ /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
309
+ * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
310
+ * `g[name]` ref — only the top-level entry is unwrapped. */
311
+ private body(c: Comb): Comb {
312
+ return c._def.tag === 'lazy' && ruleNameOf(c) !== undefined ? (lazyTarget(c) ?? c) : c
313
+ }
314
+
315
+ run(): string {
316
+ for (const [, c] of this.ruleMap) this.analyze(this.body(c), new Set())
317
+ for (const c of this.scanSkip) { this.assertNoRuleRef(c, new Set()); this.analyze(c, new Set()) }
318
+ // A shared sub-combinator (count ≥ 2) or a self-ref target gets a const, EXCEPT
319
+ // a top-level rule value (it already lives under its rule name) and EXCEPT a
320
+ // combinator lying on a self-reference cycle.
321
+ //
322
+ // A cycle's single lazy edge is the local `ref()` var bound by the enclosing
323
+ // `ref()/define()` closure, so it is only expressible INSIDE that closure. A
324
+ // hoisted const is emitted outside it, at decl scope, where the ref var does not
325
+ // exist — the back-edge then re-resolves through the cycle target's OWN const and
326
+ // the two decls reference each other eagerly. That is a genuine `const` cycle, and
327
+ // since `emitDecl` pushes the inner decl first it throws at compose time:
328
+ // const _s1 = many(choice(_s0, …)); const _s0 = (() => { … _s1 … })()
329
+ // ReferenceError: Cannot access '_s0' before initialization
330
+ // No declaration order fixes it, so cycle-interior nodes are inlined instead. They
331
+ // are re-hoisted by identity on re-lowering; the only cost is IR text, once per
332
+ // reference. `balanced()`'s interior is referenced once, so nothing existing moves.
333
+ let n = 0
334
+ for (const [c, count] of this.counts) {
335
+ if (this.ruleValues.has(c)) continue
336
+ if (this.cycleInterior.has(c)) continue
337
+ if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`)
338
+ }
339
+ // Two decl scopes. `scanSkip` is emitted FIRST into `outerDecls` (an IIFE that
340
+ // wraps the whole rules() call) because the `{ scanSkip }` options object
341
+ // precedes the `(g) => …` factory and so cannot see consts declared inside it.
342
+ // A scanSkip unit is grammar-rule free (asserted above), so every const it
343
+ // reaches is g-free and safe in the outer scope; the `emitted` guard then keeps
344
+ // each remaining (factory-only, possibly `g[name]`-referencing) const inside the
345
+ // factory. With no scanSkip, `outerDecls` stays empty and the output is
346
+ // byte-identical to the pre-scanSkip form.
347
+ const outerDecls: string[] = []
348
+ const innerDecls: string[] = []
349
+ let optionsArg = ''
350
+ if (this.scanSkip.length > 0) {
351
+ this.decls = outerDecls
352
+ const units = this.scanSkip.map(c => this.ref(c, []))
353
+ optionsArg = `{ scanSkip: [${units.join(', ')}] }, `
354
+ }
355
+ this.decls = innerDecls
356
+ const body = this.ruleMap
357
+ .map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c), [])}`)
358
+ .join(',\n')
359
+ // Shared consts go INSIDE the factory: they can reference `g[name]` rule refs,
360
+ // and `g` only exists in the factory scope.
361
+ const factory = innerDecls.length === 0
362
+ ? `(g) => ({\n${body}\n})`
363
+ : `(g) => {\n${innerDecls.map(d => ' ' + d).join('\n')}\n return ({\n${body}\n})\n}`
364
+ const call = `rules(${optionsArg}${factory})`
365
+ return outerDecls.length === 0
366
+ ? call
367
+ : `(() => {\n${outerDecls.map(d => ' ' + d).join('\n')}\n return ${call}\n})()`
368
+ }
369
+
370
+ /** Count identity references and flag self-referential subtrees. `active` is the
371
+ * set of combinators on the current DFS path (to detect a lazy pointing back). */
372
+ private analyze(c: Comb, active: Set<Comb>): void {
373
+ const def = c._def
374
+ if (def.tag === 'lazy') {
375
+ if (ruleNameOf(c) !== undefined) return // named rule ref — resolved by g[name]
376
+ const target = lazyTarget(c)
377
+ if (!target) throw new Unserializable('unresolved unnamed ref')
378
+ if (active.has(target)) { // recursion into an ancestor
379
+ this.selfRef.add(target)
380
+ // Everything on the DFS path strictly BELOW `target` is on the cycle. `active`
381
+ // is a Set built in path order, so the suffix after `target` is exactly that
382
+ // path. These must not be hoisted out of the cycle's ref()/define() closure.
383
+ let past = false
384
+ for (const a of active) {
385
+ if (past) this.cycleInterior.add(a)
386
+ else if (a === target) past = true
387
+ }
388
+ return
389
+ }
390
+ this.analyze(target, active) // inline (unnamed, non-recursive) — rare
391
+ return
392
+ }
393
+ if (def.tag === 'guard' || def.tag === 'recover' || def.tag === 'unknown') {
394
+ throw new Unserializable(`unsupported tag ${def.tag}`)
395
+ }
396
+ this.counts.set(c, (this.counts.get(c) ?? 0) + 1)
397
+ if ((this.counts.get(c) ?? 0) > 1) return // already descended
398
+ // A balanced() is an atom: its interior is derived by the constructor, so descending
399
+ // would count derived nodes and mistake its internal `self` ref for a user cycle.
400
+ // Its own skip units are authored, so they are still analyzed.
401
+ const bal = balancedOf(c)
402
+ if (bal) {
403
+ active.add(c)
404
+ for (const unit of bal.ownSkip) this.analyze(unit, active)
405
+ active.delete(c)
406
+ return
407
+ }
408
+ active.add(c)
409
+ for (const child of childrenOf(def)) this.analyze(child, active)
410
+ active.delete(c)
411
+ }
412
+
413
+ /** Reference `c`: a const var if it has one (emitting its decl first), else inline.
414
+ * `frames` are the recursive combinators currently open around this position — an
415
+ * unnamed lazy back into any of them emits that frame's ref var instead of recursing.
416
+ * A const is never cycle-interior (see `run`), so its decl is emitted with NO frames:
417
+ * decl scope cannot see any ref var, and it never needs to. */
418
+ private ref(c: Comb, frames: readonly SelfFrame[]): string {
419
+ const cn = this.constName.get(c)
420
+ if (cn) { this.emitDecl(c); return cn }
421
+ // An un-hoisted recursive target still needs its own ref()/define() closure, and
422
+ // the enclosing frames stay lexically visible inside it.
423
+ if (this.selfRef.has(c)) return this.wrap(c, frames)
424
+ return this.expr(c, frames)
425
+ }
426
+
427
+ private emitDecl(c: Comb): void {
428
+ const name = this.constName.get(c)!
429
+ if (this.emitted.has(name)) return
430
+ this.emitted.add(name) // mark first (cycle-safe)
431
+ this.decls.push(`const ${name} = ${this.wrap(c, [])}`)
432
+ }
433
+
434
+ private selfN = 0
435
+
436
+ /** Emit `c`'s own constructor call, wrapping it in a `ref()`+`.define()` closure
437
+ * if it is self-referential (an unnamed lazy in its subtree points back to it) —
438
+ * the shape `balanced()` and other recursive library combinators build. */
439
+ private wrap(c: Comb, frames: readonly SelfFrame[]): string {
440
+ if (!this.selfRef.has(c)) return this.expr(c, frames)
441
+ const rv = `_rr${this.selfN++}`
442
+ const bodyExpr = this.expr(c, [...frames, { comb: c, var: rv }])
443
+ return `(() => { const ${rv} = ref(); const _b = ${bodyExpr}; ${rv}.define(_b); return _b })()`
444
+ }
445
+
446
+ /** Emit the constructor-call source for `c` (never a const shortcut for `c` itself). */
447
+ private expr(c: Comb, frames: readonly SelfFrame[]): string {
448
+ const def = c._def
449
+ const kid = (x: Comb) => this.ref(x, frames)
450
+ // Re-emit a balanced as its constructor call so `balanced()` re-creates the ambient
451
+ // marker on the far side. `raw: true` balanceds carry no marker and stay structural,
452
+ // which is correct — they opt out of ambient resolution by definition.
453
+ const bal = balancedOf(c)
454
+ if (bal) {
455
+ const skipArg = bal.ownSkip.length === 0 ? '' : `, { skip: [${bal.ownSkip.map(kid).join(', ')}] }`
456
+ return `balanced(${JSON.stringify(bal.open)}, ${JSON.stringify(bal.close)}${skipArg})`
457
+ }
458
+ switch (def.tag) {
459
+ case 'lazy': {
460
+ const name = this.localRuleNames.get(c) ?? ruleNameOf(c)
461
+ if (name !== undefined) return `g[${JSON.stringify(name)}]`
462
+ const target = lazyTarget(c)!
463
+ // Innermost matching frame wins.
464
+ for (let i = frames.length - 1; i >= 0; i--) {
465
+ const f = frames[i]!
466
+ if (f.comb === target) return f.var
467
+ }
468
+ return this.ref(target, frames)
469
+ }
470
+ case 'literal':
471
+ return `literal(${JSON.stringify(def.value)}${def.caseInsensitive ? ', { caseInsensitive: true }' : ''})`
472
+ case 'regex':
473
+ return `regex(${JSON.stringify(def.source)}, ${JSON.stringify(def.flags)})`
474
+ case 'keywords':
475
+ return `keywords(${JSON.stringify(def.words)}, { caseInsensitive: ${def.caseInsensitive}${def.boundary !== undefined ? `, boundary: ${JSON.stringify(def.boundary)}` : ''} })`
476
+ case 'sequence':
477
+ return `sequence(${def.parsers.map(kid).join(', ')})`
478
+ case 'choice': {
479
+ // Ungated choices stay `choice(a, b, …)` byte-for-byte as before.
480
+ const gates = def.gates
481
+ if (gates.every(g => g === null)) return `choice(${def.parsers.map(kid).join(', ')})`
482
+ // A gated choice round-trips through `_gch`, which rebuilds the choice AND
483
+ // re-attaches `_def.gateSrcs` from each arm's captured gate SOURCE. Preserving
484
+ // gateSrcs is load-bearing: on re-lowering, codegen inlines the gate from its
485
+ // source (a static callback), so the re-lowered artifact stays STATICALLY
486
+ // FUSIBLE. A plain `choice({ gate: fn, … })` would rebuild the predicate as a
487
+ // source-less runtime closure → a non-static callback → `emitFusedSource`
488
+ // (macro static fusion) fails and `compose()` silently falls back to a runtime
489
+ // fuse. Without a captured source we cannot re-emit the gate → keep full pieces.
490
+ const gateSrcs = def.gateSrcs
491
+ const items = def.parsers.map((p, i) => {
492
+ if (gates[i] === null) return kid(p)
493
+ const src = gateSrcs?.[i]
494
+ if (src == null) throw new Unserializable('choice gate() has no captured source')
495
+ // `[gateSource, arm]` — `_gch` evals the source to the live predicate and
496
+ // records it as the arm's `gateSrcs` entry.
497
+ return `[${JSON.stringify(src)}, ${kid(p)}]`
498
+ })
499
+ return `_gch([${items.join(', ')}])`
500
+ }
501
+ case 'dispatch': {
502
+ const arms = def.cases.map(entry =>
503
+ `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`
504
+ )
505
+ if (def.matchers) {
506
+ for (const entry of def.matchers) {
507
+ arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`)
508
+ }
509
+ }
510
+ if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`)
511
+ return `dispatch(${kid(def.selector)}${arms.length === 0 ? '' : `, ${arms.join(', ')}`})`
512
+ }
513
+ case 'many': return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
514
+ // A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
515
+ // it through `many` so the bounds survive (oneOrMore defaults min to 1).
516
+ case 'oneOrMore': return def.min === 1 && def.max === undefined
517
+ ? `oneOrMore(${kid(def.parser)})`
518
+ : `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
519
+ case 'optional': return `optional(${kid(def.parser)})`
520
+ case 'attempt': return `attempt(${kid(def.parser)})`
521
+ // `keepSeparators` is an opt-in expressed at the SEPARATOR, not in the options
522
+ // bag, so it must round-trip as `keepSeparator(sep)` — serializing it as an
523
+ // option would reconstruct a grammar whose call site no longer states its own
524
+ // children arity, which is the exact defect this API shape exists to prevent.
525
+ case 'sepBy': return `sepBy(${kid(def.parser)}, ${def.keepSeparators ? `keepSeparator(${kid(def.separator)})` : kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`
526
+ case 'not': return `not(${kid(def.parser)})`
527
+ case 'peek': return `peek(${kid(def.parser)})`
528
+ case 'routed': return def.fallback === undefined ? 'routed()' : `routed(${kid(def.fallback)})`
529
+ case 'trivia': return `trivia(${kid(def.parser)})`
530
+ case 'token': return `token(${kid(def.parser)})`
531
+ case 'leaf': {
532
+ if (def.fnSrc === undefined) throw new Unserializable('leaf without fnSrc')
533
+ return `_lf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)})`
534
+ }
535
+ case 'label': return `label(${JSON.stringify(def.label)}, ${kid(def.parser)})`
536
+ case 'field': return `field(${JSON.stringify(def.name)}, ${kid(def.parser)})`
537
+ case 'expect': return `expect(${kid(def.parser)}${def.label !== undefined ? `, ${JSON.stringify(def.label)}` : ''})`
538
+ case 'scanTo':
539
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(', ')}]${def.raw ? ', raw: true' : ''}, orEOF: ${def.orEOF} })`
540
+ case 'transform': {
541
+ if (def.fnSrc === undefined) throw new Unserializable('transform without fnSrc')
542
+ // `_tf` sets `_def.fnSrc` so re-lowering INLINES the callback (a plain
543
+ // `transform(child, fn)` would leave fnSrc unset → a non-static runtime
544
+ // callback that emitFusedSource can't inline).
545
+ return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ', true' : ''})`
546
+ }
547
+ case 'node': {
548
+ if (def.build !== undefined && def.buildSrc === undefined) throw new Unserializable('node build without buildSrc')
549
+ const projectEntry = projectOpt(def.project)
550
+ const tagEntry = tagsOpt(def.tags)
551
+ const optEntries = [
552
+ ...(def.unwrap ? ['unwrap: true'] : []),
553
+ ...(def.collapse ? ['collapse: true'] : []),
554
+ ...(projectEntry === undefined ? [] : [projectEntry]),
555
+ ...(def.captureTrivia ? ['captureTrivia: true'] : []),
556
+ ...(def.trailingTrivia ? ['trailingTrivia: true'] : []),
557
+ // Declared or resolved arity. Carried so a re-lowered composed artifact does
558
+ // not silently re-acquire the fail-open capture cost this module resolved away.
559
+ ...(def.buildArity === undefined ? [] : [`buildArity: ${def.buildArity}`]),
560
+ ...(tagEntry === undefined ? [] : [tagEntry]),
561
+ ]
562
+ const opts = optEntries.length > 0 ? `, { ${optEntries.join(', ')} }` : ''
563
+ // `_nd` sets `_def.buildSrc` (same reason as `_tf`). No build → plain node.
564
+ if (def.type === undefined) {
565
+ if (def.buildSrc !== undefined) throw new Unserializable('inferred node build without inferred type')
566
+ return opts ? `node(${kid(def.parser)}, undefined${opts})` : `node(${kid(def.parser)})`
567
+ }
568
+ if (def.buildSrc !== undefined) {
569
+ // Trailing optionals, emitted only when present so an artifact that carries
570
+ // neither is byte-identical to what previous versions produced.
571
+ const tail: string[] = []
572
+ if (def.buildStaticError !== undefined || def.buildSigSrc !== undefined) {
573
+ if (!opts) tail.push('undefined')
574
+ tail.push(def.buildStaticError === undefined ? 'undefined' : JSON.stringify(def.buildStaticError))
575
+ }
576
+ if (def.buildSigSrc !== undefined) tail.push(JSON.stringify(def.buildSigSrc))
577
+ const trailing = tail.length > 0 ? `, ${tail.join(', ')}` : ''
578
+ return `_nd(${JSON.stringify(def.type)}, ${kid(def.parser)}, ${JSON.stringify(def.buildSrc)}${opts}${trailing})`
579
+ }
580
+ return opts ? `node(${JSON.stringify(def.type)}, ${kid(def.parser)}, undefined${opts})` : `node(${JSON.stringify(def.type)}, ${kid(def.parser)})`
581
+ }
582
+ case 'grammar': {
583
+ const classified = def.triviaParser?._meta.rootTriviaClassified === true
584
+ const spec = classified && def.triviaParser ? analyzeLabeledTrivia(def.triviaParser) : null
585
+ if (classified && !spec) throw new Unserializable('classified trivia without labeled arms')
586
+ const trivia = def.clearTrivia
587
+ ? 'null'
588
+ : spec
589
+ ? `classifiedTrivia({ ${spec.arms.map(arm => `${JSON.stringify(arm.label)}: ${kid(arm.parser)}`).join(', ')} })`
590
+ : def.triviaParser ? kid(def.triviaParser) : 'undefined'
591
+ return `parser({ trivia: ${trivia}${def.captureTrivia ? ', captureTrivia: true' : ''}${def.rootCapture ? `, rootCapture: ${JSON.stringify(def.rootCapture)}` : ''}${def.trackLines ? ', trackLines: true' : ''} }, ${kid(def.parser)})`
592
+ }
593
+ case 'withCtx': {
594
+ // A `withCtx` round-trips through `_wc`, which rebuilds it AND re-attaches
595
+ // `_def.extraSrc` from the captured `extra` SOURCE. Preserving extraSrc is
596
+ // load-bearing for the same reason as `_tf`/`_gch`: on re-lowering, codegen
597
+ // inlines the state getter from source (a static callback), so the re-lowered
598
+ // artifact stays STATICALLY FUSIBLE. Without a captured source we cannot
599
+ // re-emit the state statically → keep full pieces (interpreter fallback).
600
+ if (def.extraSrc === undefined) throw new Unserializable('withCtx without extraSrc')
601
+ return `_wc(${JSON.stringify(def.extraSrc)}, ${kid(def.parser)})`
602
+ }
603
+ case 'adjacency':
604
+ return def.polarity === 'adjacent'
605
+ ? 'adjacent()'
606
+ : def.kinds === undefined
607
+ ? 'notAdjacent()'
608
+ : `notAdjacent({ kinds: ${JSON.stringify(def.kinds)} })`
609
+ case 'guard':
610
+ case 'recover':
611
+ case 'unknown':
612
+ throw new Unserializable(`unsupported tag ${def.tag}`)
613
+ }
614
+ }
615
+ }