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,954 @@
1
+ /**
2
+ * The linker: compose independently-carried grammar pieces into ONE runnable rule map.
3
+ *
4
+ * Composition is a RULE-MAP MERGE plus a single encode. Each piece carries its
5
+ * combinator graph as IR; `compose()` evaluates every piece's IR back to a rule map,
6
+ * lets a later piece's name override an earlier one, and encodes the merged map ONCE.
7
+ * `enc.winners` binds every by-name reference against that merged map, so overriding a
8
+ * rule reroutes EVERY call to it — including calls inside a base piece's own rules
9
+ * (open recursion) — with no shared scope and no relocation. Later piece wins per
10
+ * rule name.
11
+ *
12
+ * This replaced a textual splice. The source lowering compiled each piece to namespaced
13
+ * `_r_<Name>` function sources and concatenated them into one `new Function` scope,
14
+ * patching `@FS:` first-set placeholders per winner. That is why the linker needed
15
+ * `'unsafe-eval'`; the merge is now data, so it does not.
16
+ */
17
+ import { ruleDependencies, childrenOf } from '../analysis/gating.ts'
18
+ import { FUSED_HOST_MODE, FUSED_HOST_ELIDED, type HostMode } from '../cst/host-mode.ts'
19
+ import { evalRuleMapIR, serializeRuleMap } from './ir-serialize.ts'
20
+ import { compileLinkableTable, type LinkableTable } from './compile-linkable-table.ts'
21
+ import { compileRuleMapRunnable } from '../table/compile-rule-map.ts'
22
+ import { tableRules } from '../table/assemble.ts'
23
+ import { GRAMMAR_REFLECTION } from '../cst/reflection.ts'
24
+ import { PARSEMAN_VERSION } from '../version.ts'
25
+ import type { BuildHost, Combinator, CstCollapsePredicate, ParseContext, ParseResult } from '../types.ts'
26
+ import type { Runnable } from '../functional/run.ts'
27
+
28
+ /**
29
+ * Compile a `rules()` map to a **linkable artifact** — the composable, shippable
30
+ * form (RULE_ABI_PLAN §4). A package exports `linkable(rules(g => …))`; consumers
31
+ * import that artifact and `fuse([...])` it — **no source of the base grammar is
32
+ * ever read**. Under the macro this is precompiled to static pieces; in the
33
+ * interpreter it compiles here at load (like `compile()`).
34
+ *
35
+ * `ns` is a per-artifact namespace; omit it to auto-assign a process-unique one
36
+ * (fine at runtime — the macro supplies a stable module-derived ns instead).
37
+ */
38
+ let _nsCounter = 0
39
+ export function linkable(
40
+ rulesMap: Record<string, Combinator<unknown>>,
41
+ ns?: string,
42
+ trivia?: Combinator<unknown>,
43
+ // Compile-time host mode, same meaning as `compile(g, { hostMode })`: 'ast' (default)
44
+ // emits the grammar's own builders and NO positioned-CST branch; 'cst' builds every
45
+ // node through the host. A linked/fused artifact is version- and mode-locked, so a
46
+ // language service links its own 'cst' artifact rather than switching at parse time.
47
+ hostMode?: HostMode,
48
+ ): LinkableTable {
49
+ const piece = compileLinkableTable(
50
+ Object.entries(rulesMap),
51
+ ns ?? `_lk${_nsCounter++}_`,
52
+ { ...(trivia ? { trivia } : {}), ...(hostMode ? { hostMode } : {}) },
53
+ )
54
+ if (!piece) throw new Error('linkable(): this grammar cannot be compiled to a linkable artifact (contains a runtime-only parser fallback)')
55
+ return piece
56
+ }
57
+
58
+ export type CstBuildHostOptions = {
59
+ /**
60
+ * Collapse transparent one-child CST wrapper nodes at build time.
61
+ * - `true`: collapse any one-child node whose rawChildren also has exactly one
62
+ * entry, so trivia/error boundaries are not silently dropped.
63
+ * - `string[]`: collapse only these grammar node types.
64
+ * - predicate: final policy hook for language-specific public CSTs.
65
+ */
66
+ collapse?: boolean | readonly string[] | CstCollapsePredicate
67
+ /**
68
+ * Materialize `node(..., { tags })` grammar metadata onto produced CST nodes.
69
+ * When omitted, tags stay in grammar reflection for zero per-node tree cost.
70
+ */
71
+ tags?: boolean
72
+ }
73
+
74
+ function normalizeCstCollapse(collapse: CstBuildHostOptions['collapse']): CstCollapsePredicate | undefined {
75
+ if (collapse === true) return () => true
76
+ if (Array.isArray(collapse)) {
77
+ const types = new Set(collapse)
78
+ return type => types.has(type)
79
+ }
80
+ return typeof collapse === 'function' ? collapse : undefined
81
+ }
82
+
83
+ function buildCstNode(
84
+ type: string,
85
+ children: ReadonlyArray<unknown>,
86
+ _fields: unknown,
87
+ span: { start: number; end: number },
88
+ _rawChildren?: ReadonlyArray<unknown>,
89
+ _triviaLog?: readonly number[],
90
+ state?: unknown,
91
+ tags?: readonly string[] | undefined,
92
+ ): unknown {
93
+ // Carry the grammar's `ctx.state` snapshot onto the node (null when unset) — the
94
+ // CST contract includes `state` and incremental re-parse replays it on edit.
95
+ return tags !== undefined && tags.length > 0
96
+ ? { _tag: 'node', type, tags, span: { ...span }, state: state ?? null, children: [...children] }
97
+ : { _tag: 'node', type, span: { ...span }, state: state ?? null, children: [...children] }
98
+ }
99
+
100
+ /**
101
+ * A generic positioned-CST build host (RULE_ABI_PLAN §7). Pass as `ctx.build`
102
+ * (or `parseDoc(..., { build: cstBuildHost })`) to make ANY linkable/fused
103
+ * grammar produce a uniform CST — `{ _tag:'node', type, span, state, children }`
104
+ * — instead of its own eval-AST builders. This is the host the linter and IDE
105
+ * drivers use; the eval driver leaves `ctx.build` unset (grammar's own builders).
106
+ *
107
+ * For public syntax trees, call `cstBuildHost({ collapse })`: Parseman will skip
108
+ * allocating wrapper CST nodes whose single child should stand in for the rule.
109
+ */
110
+ export function cstBuildHost(options?: CstBuildHostOptions): BuildHost
111
+ export function cstBuildHost(
112
+ type: string,
113
+ children: ReadonlyArray<unknown>,
114
+ fields: unknown,
115
+ span: { start: number; end: number },
116
+ rawChildren?: ReadonlyArray<unknown>,
117
+ triviaLog?: readonly number[],
118
+ state?: unknown,
119
+ tags?: readonly string[] | undefined,
120
+ ): unknown
121
+ export function cstBuildHost(
122
+ typeOrOptions?: string | CstBuildHostOptions,
123
+ children?: ReadonlyArray<unknown>,
124
+ _fields?: unknown,
125
+ span?: { start: number; end: number },
126
+ rawChildren?: ReadonlyArray<unknown>,
127
+ triviaLog?: readonly number[],
128
+ state?: unknown,
129
+ _tags?: readonly string[] | undefined,
130
+ ): unknown {
131
+ if (typeof typeOrOptions === 'string') {
132
+ return buildCstNode(typeOrOptions, children ?? [], _fields, span ?? { start: 0, end: 0 }, rawChildren, triviaLog, state)
133
+ }
134
+ const collapse = normalizeCstCollapse(typeOrOptions?.collapse)
135
+ const materializeTags = typeOrOptions?.tags === true
136
+ const host: BuildHost = (
137
+ type: string,
138
+ children: ReadonlyArray<unknown> | undefined,
139
+ fields: unknown,
140
+ span: { start: number; end: number },
141
+ rawChildren: ReadonlyArray<unknown>,
142
+ triviaLog: readonly number[],
143
+ state: unknown,
144
+ tags?: readonly string[] | undefined,
145
+ // A CST/collapse host always keeps `children` (chV) — the opt-out never
146
+ // applies — so `?? []` is unreachable defensive modeling for the widened type.
147
+ ) => buildCstNode(type, children ?? [], fields, span, rawChildren, triviaLog, state, materializeTags ? tags : undefined)
148
+ ;(host as typeof host & { _parsemanCstOutput?: true })._parsemanCstOutput = true
149
+ if (collapse) host._parsemanCstCollapse = collapse
150
+ return host
151
+ }
152
+
153
+ // `cstBuildHost` itself is also accepted as a BuildHost (without options).
154
+ ;(cstBuildHost as unknown as { _parsemanCstOutput?: true })._parsemanCstOutput = true
155
+
156
+ /**
157
+ * A fused function receives the full ParseContext through `run()`. Direct
158
+ * callers historically supplied a plain context object, so keep that usage
159
+ * valid while making the function assignable to the public `Runnable` type.
160
+ * Generated code treats optional framework fields as absent when they are not
161
+ * provided, matching the interpreter's normal defaults.
162
+ */
163
+ export type FusedRule = (
164
+ input: string,
165
+ pos: number,
166
+ ctx: ParseContext | Record<string, unknown>,
167
+ ) => ParseResult<unknown> & { readonly value?: unknown }
168
+
169
+
170
+ /**
171
+ * Fuse carried pieces into a runnable rule map — the TABLE equivalent of the
172
+ * textual splice this file used to perform.
173
+ *
174
+ * `fusedBody()` concatenated namespaced `_r_<Name>` function sources, picked a winning
175
+ * function per name, and patched `@FS:` dispatch placeholders with the winner's
176
+ * first-set condition — roughly 200 lines whose entire job was to make separately
177
+ * lowered SOURCE agree about names. A table has no text to splice, so the merge moves
178
+ * up one level onto the combinators: merge the rule maps (later piece wins), then
179
+ * `encodeTable` ONCE over the merged map. `enc.winners` binds every by-name reference,
180
+ * including a base piece's internal `g.Atom` that an override replaced, so open
181
+ * recursion across pieces resolves without relocating a single encoded offset.
182
+ *
183
+ * One encode, no pools to merge, and the result is the table the merged grammar would
184
+ * have produced had it been written as a single `rules()` call.
185
+ */
186
+ function fuseCarried(
187
+ carried: ReadonlyArray<LinkableTable | IRPiece>,
188
+ trivia?: Combinator<unknown>,
189
+ hostMode?: HostMode,
190
+ ): Record<string, FusedRule> {
191
+ const maps: Array<Array<[string, Combinator<unknown>]>> = []
192
+ for (const p of carried) {
193
+ // ARTIFACT VERSION LOCK. `fusedBody` enforced this and would have taken it with it:
194
+ // artifacts are version-locked and there is no cross-version read path, so an
195
+ // UNSTAMPED piece and a MISMATCHED one are both refused — a stale artifact that
196
+ // merely happens to still encode is exactly what this stops.
197
+ if (!isIRPiece(p)) {
198
+ if (typeof p.v !== 'string') {
199
+ throw new Error(
200
+ `parseman: artifact "${p.ns}" is UNSTAMPED (compiled before the version-lock invariant). `
201
+ + `Recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse unversioned or cross-version artifacts.`,
202
+ )
203
+ }
204
+ if (p.v !== PARSEMAN_VERSION) {
205
+ throw new Error(
206
+ `parseman: artifact "${p.ns}" was compiled with parseman ${p.v}, but is being fused with parseman ${PARSEMAN_VERSION}. `
207
+ + `Compiled grammar artifacts are version-locked — recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse across versions.`,
208
+ )
209
+ }
210
+ }
211
+ const rules = ruleMapOfCarried(p)
212
+ if (rules === undefined) {
213
+ throw new Error(`compose: carried piece "${p.ns}" has no re-lowerable IR and cannot be fused`)
214
+ }
215
+ maps.push(rules)
216
+ }
217
+ const merged = mergeCarriedRuleMaps(maps)
218
+ materializeDirectBuilders(merged)
219
+ // COMPOSING-WINS is an OVERRIDE, not a gap-fill: the composing grammar's trivia
220
+ // governs every fused rule INCLUDING inherited ones. `applyAmbient` inside
221
+ // `compileRuleMap` only fills a rule that declares none, which would leave an
222
+ // inherited rule still skipping its own base's whitespace after a delta re-declared
223
+ // it. Safe to mutate — `evalRuleMapIR` builds fresh combinators per fuse.
224
+ if (trivia) {
225
+ for (const [, rule] of merged) {
226
+ if (rule._meta.isTrivia) continue
227
+ ;(rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = trivia
228
+ }
229
+ }
230
+ // RUNNABLE, not printable. `compose()` returns a parser; it never emits source, so
231
+ // requiring a captured source per author callback would refuse every grammar built at
232
+ // runtime — which have live callbacks by construction.
233
+ const refusals: string[] = []
234
+ const compiled = compileRuleMapRunnable(merged, {
235
+ ...(trivia ? { trivia } : {}),
236
+ ...(hostMode ? { hostMode } : {}),
237
+ refusals,
238
+ })
239
+ if (compiled === null) {
240
+ throw new Error(`compose: the merged grammar could not be encoded to a table${refusals.length ? ` — ${refusals.join('; ')}` : ''}`)
241
+ }
242
+ // `tableRules`, NOT `exec.ts`'s same-named `tableRules`. Both return
243
+ // `Record<string, TableRule>`, so binding the interpreter here type-checked and ran
244
+ // correctly — it was just the SLOW engine, on the one path (compose/fuse) that never
245
+ // goes through `table/index.ts` and so never saw the `tableRules as tableRules`
246
+ // re-export. Import the assembler by its own name so the binding cannot go stale again.
247
+ const map = tableRules(compiled.prog) as unknown as Record<string, FusedRule>
248
+ // The host-mode stamp went on the fused closure before; a table carries nothing until
249
+ // it is stamped, and an UNSTAMPED map reads as `{ ast, false }` so every driver
250
+ // compatibility check passes vacuously. Stamped on the rule functions too, because
251
+ // `run(map.Rule, …)` is handed the rule and never sees the map.
252
+ for (const k of Object.keys(map)) {
253
+ Object.defineProperty(map[k]!, FUSED_HOST_MODE, { value: compiled.hostMode, enumerable: false })
254
+ Object.defineProperty(map[k]!, FUSED_HOST_ELIDED, { value: compiled.hostBranchElided, enumerable: false })
255
+ }
256
+ Object.defineProperty(map, FUSED_HOST_MODE, { value: compiled.hostMode, enumerable: false })
257
+ Object.defineProperty(map, FUSED_HOST_ELIDED, { value: compiled.hostBranchElided, enumerable: false })
258
+ // GRAMMAR REFLECTION, which `fusedBody` merged across pieces and stamped. A visitor
259
+ // built over a composed grammar reads it to know the node types; unstamped, it reads
260
+ // as an empty grammar and every visitor silently matches nothing.
261
+ Object.defineProperty(map, GRAMMAR_REFLECTION, { value: compiled.reflection, enumerable: false })
262
+ return map
263
+ }
264
+
265
+ /**
266
+ * Re-attach a DIRECT node builder that came back from IR as an inert sentinel.
267
+ *
268
+ * `evalRuleMapIR` deliberately refuses to evaluate a captured `buildSrc`: raw IR
269
+ * interpretation must not run arbitrary source, so `_nd` installs a thrower and keeps
270
+ * the text. Only a COMPILER consumer may materialize it — and the runtime fuse is one.
271
+ * The source lowering did exactly this, less visibly: it inlined `buildSrc` into the
272
+ * fused body and `new Function` evaluated it. The table parks callbacks in a pool
273
+ * instead of inlining them, so the same materialization has to be explicit or the
274
+ * sentinel reaches the pool and throws on the first parse through that node.
275
+ *
276
+ * Scoped to the runtime fuse. The MACRO never comes here: its encoder captures
277
+ * `buildSrc` and PRINTS it, so it needs no live function at all.
278
+ */
279
+ function materializeDirectBuilders(ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>): void {
280
+ const seen = new Set<Combinator<unknown>>()
281
+ const visit = (p: Combinator<unknown>): void => {
282
+ if (seen.has(p)) return
283
+ seen.add(p)
284
+ const d = p._def as { tag: string; build?: unknown; buildSrc?: string }
285
+ if (d.tag === 'node' && typeof d.buildSrc === 'string') {
286
+ try {
287
+ // eslint-disable-next-line no-eval
288
+ const fn = (0, eval)(`(${d.buildSrc})`) as unknown
289
+ if (typeof fn === 'function') d.build = fn
290
+ } catch { /* leave the sentinel: it throws with its own message on reach */ }
291
+ }
292
+ if (d.tag === 'lazy') {
293
+ let resolved: Combinator<unknown> | undefined
294
+ try { resolved = (d as unknown as { thunk: () => Combinator<unknown> }).thunk() } catch { return }
295
+ if (resolved) visit(resolved)
296
+ return
297
+ }
298
+ for (const child of childrenOf(p._def)) visit(child)
299
+ }
300
+ for (const [, rule] of ruleMap) visit(rule)
301
+ }
302
+
303
+ /** The combinator map behind a carried piece: IR is evaluated back, a table piece
304
+ * uses the IR it always carries. `undefined` only for a piece with neither. */
305
+ function ruleMapOfCarried(p: LinkableTable | IRPiece): Array<[string, Combinator<unknown>]> | undefined {
306
+ if (isIRPiece(p)) return evalRuleMapIR(p.ir)
307
+ // IR FIRST: re-evaluating it yields FRESH combinators, so seeding composing trivia
308
+ // onto their `_meta` cannot leak back into the artifact this piece came from. The live
309
+ // map is the fallback for a runtime-built grammar that has no serializable IR.
310
+ if (p.ir !== null) return evalRuleMapIR(p.ir)
311
+ return p.ruleMap.length > 0 ? p.ruleMap.map(([k, v]) => [k, v] as [string, Combinator<unknown>]) : undefined
312
+ }
313
+
314
+ /** Fold ordered rule maps into the composed map: a later piece's name WINS.
315
+ *
316
+ * A REFERENCE IS NOT A DEFINITION. A `rules(g => …)` cache also holds every `g.X` that
317
+ * was merely ACCESSED, as an unresolvable lazy. Merged in order, such an entry lands
318
+ * last and SHADOWS the piece that actually defines the name — the encoder then finds a
319
+ * hole where the winner should be and refuses the whole grammar. The reference is not
320
+ * lost: it stays inside the referring piece's rule bodies, where `enc.winners` binds it
321
+ * by name to whichever piece supplies the definition.
322
+ */
323
+ function mergeCarriedRuleMaps(
324
+ maps: ReadonlyArray<ReadonlyArray<readonly [string, Combinator<unknown>]>>,
325
+ ): Array<[string, Combinator<unknown>]> {
326
+ const winners = new Map<string, Combinator<unknown>>()
327
+ for (const map of maps) {
328
+ for (const [name, rule] of map) {
329
+ if (rule._def.tag === 'lazy') {
330
+ try { rule._def.thunk() } catch { continue }
331
+ }
332
+ winners.set(name, rule)
333
+ }
334
+ }
335
+ return [...winners]
336
+ }
337
+
338
+
339
+ export { FUSED_HOST_MODE, FUSED_HOST_ELIDED } from '../cst/host-mode.ts'
340
+
341
+ /** The host mode a fused/composed rule map was built for. Defaults to 'ast'. */
342
+ export function fusedHostModeOf(registry: object): HostMode {
343
+ const m = (registry as Record<symbol, unknown>)[FUSED_HOST_MODE]
344
+ return m === 'cst' ? 'cst' : 'ast'
345
+ }
346
+
347
+ /** Whether a fused/composed rule map dropped any direct builder's CST branch. */
348
+ export function fusedHostElidedOf(registry: object): boolean {
349
+ return (registry as Record<symbol, unknown>)[FUSED_HOST_ELIDED] === true
350
+ }
351
+
352
+ /**
353
+ * Compose grammars/artifacts into a runnable parser map — the ONLY public
354
+ * composition entry point. `compose([base, ext, …])`: later entries override
355
+ * earlier ones by rule name, and because fusion re-binds every reference in one
356
+ * shared scope, an override reroutes the base's OWN calls too (open recursion).
357
+ *
358
+ * Each entry may be a **grammar** (a `rules()` result — a map of combinators,
359
+ * linkable-ified here) OR an already-compiled **linkable artifact** (what the
360
+ * macro emits and a package ships). So a package needs no opt-in wrapper to be
361
+ * composable — `compose([importedGrammar, myRules])` just works.
362
+ *
363
+ * The macro compiles `compose([...])` to STATIC fused source (no `new Function`).
364
+ * Called at runtime (no macro, like `compile()`) it fuses via `new Function`.
365
+ */
366
+ /** A composed parser carries its flattened source pieces (non-enumerable) so it
367
+ * can be composed AGAIN — `compose([lessGrammar, delta])` where `lessGrammar` is
368
+ * itself a `compose([...])` result. */
369
+ export const COMPOSED_PIECES = Symbol.for('parseman.composedPieces')
370
+
371
+ /** The carried pieces a `compose()`/`composeLeaf()` result holds, or `undefined` when
372
+ * the value is not a composed grammar. This is what makes a fused grammar analysable:
373
+ * the pieces are re-lowerable IR even though the fused map itself is only functions. */
374
+ export function composedPiecesOf(
375
+ grammar: Record<string, unknown>,
376
+ ): ReadonlyArray<LinkableTable | IRPiece> | undefined {
377
+ const pieces = (grammar as unknown as Record<symbol, unknown>)[COMPOSED_PIECES]
378
+ return Array.isArray(pieces) ? pieces as ReadonlyArray<LinkableTable | IRPiece> : undefined
379
+ }
380
+
381
+ /**
382
+ * A terminal fused grammar may be used to run a parser, but not as an input to
383
+ * another composition. Macro `composeLeaf()` uses this for a local semantic
384
+ * reduction over imported recognition-only IR: the local reductions stay in
385
+ * their lexical module and therefore never become carried IR.
386
+ */
387
+ const LEAF_COMPOSED = Symbol.for('parseman.leafComposed')
388
+
389
+ /** The composing (outermost) trivia a runtime `compose()` applied — stored so a
390
+ * LATER `compose([thisResult, …])` that declares no trivia of its own still re-lowers
391
+ * these rules under the SAME trivia (composing-wins survives re-composition). The
392
+ * carried IR pieces hold no trivia of their own, so it must be remembered separately. */
393
+ const COMPOSED_TRIVIA = Symbol.for('parseman.composedTrivia')
394
+
395
+ /** Final winner map for semantic-coverage tooling. It exists only when every
396
+ * carried compose piece is re-lowerable IR; opaque precompiled artifacts have no
397
+ * combinator graph to inspect and therefore deliberately expose no fake map. */
398
+ const COMPOSED_COVERAGE_RULES = Symbol.for('parseman.composedCoverageRules')
399
+
400
+ /** The compact IR form a grammar carries instead of its lowered rule source: the
401
+ * combinator-construction expression, re-lowered here at fuse time. */
402
+ export type IRPiece = { ns: string; ir: string; trackLines?: true }
403
+
404
+ function isIRPiece(p: unknown): p is IRPiece {
405
+ return !!p && typeof p === 'object'
406
+ && typeof (p as IRPiece).ir === 'string' && typeof (p as IRPiece).ns === 'string'
407
+ && !('keys' in (p as object))
408
+ }
409
+
410
+ /** A `linkable()` artifact — a TABLE piece. Distinguished from a bare IR piece by the
411
+ * fields only a compiled artifact has (`keys`/`external`), and from a plain `rules()`
412
+ * map by carrying `ns` at all. */
413
+ function isLinkableTable(p: unknown): p is LinkableTable {
414
+ return !!p && typeof p === 'object'
415
+ && typeof (p as LinkableTable).ns === 'string'
416
+ && Array.isArray((p as LinkableTable).keys)
417
+ }
418
+
419
+ /** Memoize a zero-arg thunk, keeping it LAZY. Used where two diagnostic thunks want
420
+ * the same carried-IR hydration: the work must not happen when the diagnostic is off,
421
+ * and must not happen twice when it is on. */
422
+ export function once<T>(fn: () => T): () => T {
423
+ let done = false
424
+ let value: T
425
+ return () => {
426
+ if (!done) { value = fn(); done = true }
427
+ return value
428
+ }
429
+ }
430
+
431
+ /** The re-lowerable carried pieces' rule maps, in compose order — the input to the
432
+ * gating analysis (`diagnoseGrammar`). An opaque precompiled artifact contributes no
433
+ * combinator graph, so it is skipped: a hole it would have bound stays unresolved
434
+ * and its choice stays deferred, never falsely warned.
435
+ *
436
+ * Skipping is not the same as having nothing to say. Use `carriedRuleMapsDetailed`
437
+ * where the skip must be REPORTED — a diagnostic that drops part of the grammar and
438
+ * then returns a clean result is indistinguishable from one that verified it. */
439
+ export function carriedRuleMaps(carried: ReadonlyArray<LinkableTable | IRPiece>): Array<Array<[string, Combinator<unknown>]>> {
440
+ return carriedRuleMapsDetailed(carried).maps
441
+ }
442
+
443
+ /** `carriedRuleMaps` plus the pieces it could NOT re-lower, named by namespace and
444
+ * rule count, so a caller can report exactly how much of the grammar went unseen. */
445
+ export function carriedRuleMapsDetailed(
446
+ carried: ReadonlyArray<LinkableTable | IRPiece>,
447
+ ): { maps: Array<Array<[string, Combinator<unknown>]>>; opaque: Array<{ ns: string; ruleNames: string[] }> } {
448
+ const maps: Array<Array<[string, Combinator<unknown>]>> = []
449
+ const opaque: Array<{ ns: string; ruleNames: string[] }> = []
450
+ for (const p of carried) {
451
+ // A table artifact is recoverable far more often than a source one was: it carries
452
+ // IR, and failing that the live combinators. Ask for the map before declaring the
453
+ // piece opaque, or a perfectly analysable grammar is reported as unseen.
454
+ const rules = ruleMapOfCarried(p)
455
+ if (rules !== undefined) { maps.push(rules); continue }
456
+ // NAMED, not anonymous. `keys` is the artifact's rule-name list; the field this
457
+ // used to read (`ruleFns`) belonged to the source lowering, and reading a missing
458
+ // one degrades every opaque piece to `<artifact _lkN_>` — "reported, but uselessly",
459
+ // which is the exact failure this reporting exists to prevent.
460
+ opaque.push({ ns: p.ns, ruleNames: isIRPiece(p) ? [] : [...p.keys] })
461
+ }
462
+ return { maps, opaque }
463
+ }
464
+
465
+ /**
466
+ * Recover the override-winner COMBINATOR map behind a `compose()` result, plus the
467
+ * pieces that could not be recovered.
468
+ *
469
+ * A fused map holds rule functions, so any consumer that walks a combinator graph
470
+ * (gating analysis, the spec/EBNF/railroad model) cannot read it directly. The graph
471
+ * is not lost, though — `compose()` retains re-lowerable IR — so this is the single
472
+ * shared recovery both consumers use. Sharing it is the point: two copies of this
473
+ * logic is how one walker gets fixed and the other silently keeps failing.
474
+ *
475
+ * Returns `undefined` when `grammar` is not a composed result.
476
+ */
477
+ export function recoverComposedRules(
478
+ grammar: Record<string, unknown>,
479
+ ): { rules: Map<string, Combinator<unknown>>; opaque: Array<{ ns: string; ruleNames: string[] }> } | undefined {
480
+ const carried = composedPiecesOf(grammar)
481
+ if (carried === undefined) return undefined
482
+ const { maps, opaque } = carriedRuleMapsDetailed(carried)
483
+ const rules = new Map<string, Combinator<unknown>>()
484
+ // Later wins, matching the linker's own fuse semantics. An accessed-but-undefined
485
+ // `g.X` leaks in as an unresolved lazy — a REFERENCE, not a definition — and must
486
+ // never shadow the artifact that really defines X.
487
+ for (const map of maps) for (const [name, rule] of map) {
488
+ if (rule._def.tag === 'lazy') { try { rule._def.thunk() } catch { continue } }
489
+ rules.set(name, rule)
490
+ }
491
+ return { rules, opaque }
492
+ }
493
+
494
+ function coverageRulesOf(carried: Array<LinkableTable | IRPiece>): Record<string, Combinator<unknown>> | undefined {
495
+ const winners: Record<string, Combinator<unknown>> = {}
496
+ for (const piece of carried) {
497
+ if (!isIRPiece(piece)) return undefined
498
+ const map = evalRuleMapIR(piece.ir)
499
+ for (const [name, rule] of map) {
500
+ // An accessed-but-undefined `g.Name` is an external reference, never a
501
+ // rule definition. Match compose's IR filtering rule exactly.
502
+ if (rule._def.tag === 'lazy') {
503
+ try { rule._def.thunk() } catch { continue }
504
+ }
505
+ winners[name] = rule
506
+ }
507
+ }
508
+ return winners
509
+ }
510
+
511
+ /** Return the final override-winner combinator map carried by runtime
512
+ * `compose()`, or `undefined` when a precompiled opaque artifact participated.
513
+ * This is intentionally internal: callers must not treat it as a parser API. */
514
+ export function composedCoverageRules(grammar: Record<string, unknown>): Record<string, Combinator<unknown>> | undefined {
515
+ return (grammar as Record<symbol, unknown>)[COMPOSED_COVERAGE_RULES] as Record<string, Combinator<unknown>> | undefined
516
+ }
517
+
518
+ /** Flatten one `compose()` item to its pieces: a prior composed result → its
519
+ * carried list; an artifact → itself; a grammar (`rules()` map) → linkable-ified. */
520
+ function nextComposeNs(used: Set<string>): string {
521
+ let ns: string
522
+ do { ns = `_lk${_nsCounter++}_` } while (used.has(ns))
523
+ used.add(ns)
524
+ return ns
525
+ }
526
+
527
+ /** Flatten one `compose()` item to its RE-LOWERABLE carried items — the form stored
528
+ * on the composed result so it can be composed AGAIN under a NEW composing trivia.
529
+ * A grammar (`rules()` map) is carried as compact IR (`{ns, ir}`), NOT baked source,
530
+ * so a later `compose([thisResult, delta])` re-lowers it with the delta's trivia
531
+ * (multi-level composing-wins). A prior composed result contributes its OWN carried
532
+ * items (already IR); a pre-compiled artifact has no source, so it stays baked. */
533
+ function itemCarried(
534
+ item: LinkableTable | Record<string, unknown>,
535
+ used: Set<string>,
536
+ trivia?: Combinator<unknown>,
537
+ // Only reaches the non-serializable fallback below, where the grammar is baked
538
+ // immediately instead of carried as re-lowerable IR.
539
+ hostMode?: HostMode,
540
+ ): Array<LinkableTable | IRPiece> {
541
+ const carried = (item as Record<symbol, unknown>)[COMPOSED_PIECES]
542
+ // A prior composed result (runtime or macro-compiled): its carried list is already
543
+ // re-lowerable (IR pieces, plus any pre-compiled artifacts). Pass it through so THIS
544
+ // compose re-lowers it under its own composing trivia. Reserve its namespaces so a
545
+ // sibling grammar map can't collide with them.
546
+ if (Array.isArray(carried)) {
547
+ const items = carried as Array<LinkableTable | IRPiece>
548
+ for (const p of items) used.add(p.ns)
549
+ return items
550
+ }
551
+ // A pre-compiled artifact (`linkable()`): a table piece. It ALWAYS carries its IR,
552
+ // which is what makes table-to-table composition a rule-map merge rather than a
553
+ // relocation of two encoded programs — so unlike a source artifact it stays
554
+ // re-lowerable under a new composing trivia.
555
+ if (isLinkableTable(item)) {
556
+ used.add(item.ns)
557
+ return [item]
558
+ }
559
+ // A grammar (`rules()` map): carry it as compact IR so a later compose re-lowers it
560
+ // under ITS trivia. Unserializable → bake now with this compose's trivia (can't
561
+ // re-lower later; acceptable fallback, mirrors the macro's full-pieces fallback).
562
+ const map = item as Record<string, Combinator<unknown>>
563
+ const ns = nextComposeNs(used)
564
+ // Drop EXTERNAL entries first (same filter as compileLinkable): a `rules()` cache
565
+ // also holds every ACCESSED-but-undefined `g.X` as an unresolved-lazy entry. Left in,
566
+ // serializeRuleMap would emit `X: g["X"]` — a self-referential rule that shadows the
567
+ // sibling artifact defining X and recurses forever. They resolve by name at fuse time.
568
+ const entries = Object.entries(map).filter(([, val]) => {
569
+ const d = val._def
570
+ if (d.tag !== 'lazy') return true
571
+ try { d.thunk(); return true } catch { return false }
572
+ })
573
+ // Carry this grammar's ambient `scanSkip` (per-piece — opaque units are
574
+ // dialect-specific, NOT composing-wins) into the IR so a re-lower stamps
575
+ // `grammarScanSkip` back on. `linkable()`'s fallback reads it off `_meta` directly.
576
+ const scanSkip = entries
577
+ .map(([, val]) => (val._meta as { grammarScanSkip?: Combinator<unknown>[] }).grammarScanSkip)
578
+ .find(Boolean)
579
+ const ir = serializeRuleMap(entries, scanSkip)
580
+ return ir ? [{ ns, ir }] : [linkable(map, ns, trivia, hostMode)]
581
+ }
582
+
583
+ /** The composed grammar's ambient trivia = the LAST composed item that declares a
584
+ * grammar-level trivia (via `rules({ trivia }, …)`, which tags `grammarTrivia` on its
585
+ * rules). Outermost wins: the composing grammar's trivia applies to every fused rule,
586
+ * including those inherited from a base — so e.g. an SCSS `rw` (which extends Less's)
587
+ * governs the inherited Less/CSS rules too. `parser`/`noTrivia` still override locally. */
588
+ function composingTriviaOf(items: Array<LinkableTable | Record<string, unknown>>): Combinator<unknown> | undefined {
589
+ for (let i = items.length - 1; i >= 0; i--) {
590
+ const item = items[i] as Record<string, unknown> | undefined
591
+ if (!item || isLinkableTable(item)) continue
592
+ // A COMPOSED item states its trivia on the stamp rather than on its values (its
593
+ // rules live in carried IR, which does not carry `_meta`). Skipping such an item
594
+ // entirely — as this did — loses the trivia of every grammar that reached this
595
+ // compose through a PRIOR compose.
596
+ const stamped = (item as Record<symbol, unknown>)[COMPOSED_TRIVIA] as Combinator<unknown> | undefined
597
+ if (stamped) return stamped
598
+ if ((item as Record<symbol, unknown>)[COMPOSED_PIECES]) continue
599
+ for (const v of Object.values(item)) {
600
+ const t = (v as Combinator<unknown> | undefined)?._meta?.grammarTrivia
601
+ if (t) return t
602
+ }
603
+ }
604
+ return undefined
605
+ }
606
+
607
+ export function compose(
608
+ items: Array<LinkableTable | Record<string, unknown>>,
609
+ /**
610
+ * Compile-time host mode for the fused artifact, same meaning as
611
+ * `compile(g, { hostMode })`. Omit (or `'ast'`) for the eval driver — the fused rules
612
+ * build through the grammar's own `build` callbacks and carry no positioned-CST
613
+ * branch. Pass `'cst'` to fuse a SECOND artifact from the same pieces for the linter /
614
+ * IDE / language-service driver. Two compilations of one grammar, decided here, rather
615
+ * than one artifact deciding per node on every parse.
616
+ */
617
+ opts?: { hostMode?: HostMode },
618
+ ): Record<string, FusedRule> {
619
+ if (items.some(item => (item as Record<symbol, unknown>)[LEAF_COMPOSED] === true)) {
620
+ throw new Error('compose: a composeLeaf() result is terminal and cannot be composed again')
621
+ }
622
+ const used = new Set<string>()
623
+ // The composed grammar's ambient trivia comes from the composing grammar itself —
624
+ // whatever the last piece declared via rules({ trivia }, …). No separate option:
625
+ // the trivia rides with the grammar that declared it.
626
+ const trivia = composingTriviaOf(items)
627
+ // Carried items are RE-LOWERABLE (IR); materialize them ONCE with this compose's
628
+ // trivia for the now-fuse, but STORE the un-materialized carried list so a later
629
+ // compose can re-lower it under a different trivia (multi-level composing-wins).
630
+ const carried = items.flatMap(item => itemCarried(item, used, trivia, opts?.hostMode))
631
+ const map = fuseCarried(carried, trivia, opts?.hostMode)
632
+ Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false })
633
+ if (trivia) Object.defineProperty(map, COMPOSED_TRIVIA, { value: trivia, enumerable: false })
634
+ const coverageRules = coverageRulesOf(carried)
635
+ if (coverageRules) Object.defineProperty(map, COMPOSED_COVERAGE_RULES, { value: coverageRules, enumerable: false })
636
+ return map
637
+ }
638
+
639
+ /**
640
+ * Compose a terminal grammar. This is for a leaf parser that overlays local
641
+ * semantic reductions on reusable recognition rules.
642
+ *
643
+ * Under the macro this lowers to STATIC fused source (functions), exactly like
644
+ * `compose()`. It is still macro-only as a *compiled* artifact: without macro
645
+ * lowering there is no safe way to keep lexical builders out of carried IR, so it
646
+ * never falls back to runtime CODEGEN composition.
647
+ *
648
+ * Called at runtime (no macro) it returns the INTERPRETED fuse of the same items —
649
+ * a combinator map, not a map of compiled functions (`fuseInterpreted`), fused lazily
650
+ * per rule name.
651
+ *
652
+ * THE RETURN TYPE IS `Runnable`, NOT `FusedRule`, BECAUSE BOTH PATHS ARE REAL. A macro
653
+ * build yields fused functions; an un-macro'd call yields combinators. `Runnable` is
654
+ * already the library's name for "either of those" — it is what `run()` and
655
+ * `parseDoc()` take — so the declared type is TRUE on both paths and a caller needs no
656
+ * narrowing to use the result. This used to declare `Record<string, FusedRule>` and
657
+ * launder the runtime path through an `as unknown as`, which let a caller hold a
658
+ * combinator map while the type promised compiled functions.
659
+ *
660
+ * Do NOT "fix" this by deleting the runtime path. It is load-bearing: the `bench/jess`
661
+ * harness family and two differential-gate legs (`emit-identity-one`,
662
+ * `scan-shape-oracle-one`) import un-macro'd grammar modules and depend on this lazy
663
+ * interpreted fuse, one dialect per process. Whether an un-macro'd `composeLeaf()`
664
+ * should exist at all is a separate, open owner question — but while the gates depend
665
+ * on it, it exists, and the type says so.
666
+ */
667
+ export function composeLeaf(
668
+ items: Array<LinkableTable | Record<string, unknown>>,
669
+ ): Record<string, Runnable> {
670
+ const pieces = items.flatMap(interpretedPieces)
671
+ let fused: Record<string, Combinator<unknown>> | undefined
672
+ const map: Record<string, unknown> = {}
673
+ // LAZY on purpose. A grammar module typically builds SEVERAL leaf grammars over
674
+ // one shared recognition piece (`cssGrammar`, `cssLineGrammar`, `cssCstGrammar`,
675
+ // …). An interpreted fuse binds that shared piece IN PLACE, so only one of them
676
+ // can exist at a time — fusing all of them at import would make merely importing
677
+ // the module throw. Fusing on first ACCESS means the grammar you actually use
678
+ // works, and reaching for a second, conflicting one fails loudly at that point.
679
+ // (`trackLines`/`hostMode` are compile-time distinctions; the interpreter decides
680
+ // both per parse, so those variants are the same interpreted grammar anyway.)
681
+ for (const name of ruleNamesOf(pieces)) {
682
+ Object.defineProperty(map, name, {
683
+ enumerable: true,
684
+ configurable: true,
685
+ get: () => (fused ??= fusePieces(pieces))[name],
686
+ })
687
+ }
688
+ Object.defineProperty(map, LEAF_COMPOSED, { value: true, enumerable: false })
689
+ Object.defineProperty(map, INTERPRETED_PIECES, {
690
+ value: pieces.filter(p => p.plain).map(p => p.entries),
691
+ enumerable: false,
692
+ })
693
+ return map as Record<string, Runnable>
694
+ }
695
+
696
+ /* ── Interpreted fuse ─────────────────────────────────────────────────────────
697
+ *
698
+ * `compose()` fuses by CODEGEN: every piece is compiled to `_r_<Name>` functions
699
+ * dropped into one scope, so a reference resolves by NAME and an override reroutes
700
+ * the base piece's own calls (open recursion). None of that exists interpreted —
701
+ * the interpreter runs the combinator graph, and a cross-piece reference is an
702
+ * ordinary `ref()` placeholder that nobody ever `.define()`d. That is why a
703
+ * composed grammar could not be run interpreted at all, and why every diagnostic
704
+ * that must NOT reach codegen (profiling, gating analysis, coverage) had to be
705
+ * hand-fused in throwaway scripts.
706
+ *
707
+ * The interpreted fuse binds those placeholders directly, with the SAME semantics
708
+ * the compiled fuse gets from name resolution:
709
+ * - later piece wins per rule name (matching the compiled merge);
710
+ * - an override REPOINTS the slot every call site already holds, so a base
711
+ * piece's internal calls reroute too (open recursion);
712
+ * - the composing grammar's trivia governs every fused rule (`composingTriviaOf`);
713
+ * - a referenced-but-undefined rule is a fuse-time error, not a parse-time one.
714
+ *
715
+ * It is MUTATING by construction: a hole is a shared object, and binding it is the
716
+ * only way its call sites can see the answer. `repointRef` therefore records what
717
+ * it changed and refuses a CONFLICTING second bind, so two different fusions over
718
+ * one shared piece fail loudly instead of silently rewriting each other's parser.
719
+ * ───────────────────────────────────────────────────────────────────────────── */
720
+
721
+ /**
722
+ * A rule slot produced by `ref()` — what `rules()` stores for every rule that is
723
+ * referenced by name, and for every `g.X` hole a piece leaves for another piece to
724
+ * fill. `parse`/`_def.thunk` are OWN properties of that object, which is what lets
725
+ * the interpreted fuse repoint it in place.
726
+ */
727
+ type RefSlot = Combinator<unknown> & {
728
+ _def: { tag: 'lazy'; thunk: () => Combinator<unknown> }
729
+ define(p: Combinator<unknown>): void
730
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown>
731
+ }
732
+
733
+ function isRefSlot(c: Combinator<unknown>): c is RefSlot {
734
+ return c._def.tag === 'lazy' && typeof (c as unknown as { define?: unknown }).define === 'function'
735
+ }
736
+
737
+ /** What a slot resolved to BEFORE any interpreted fuse touched it (`null` = it was
738
+ * an unbound cross-piece hole). Recorded on first repoint so a LATER fuse computes
739
+ * its winner from the grammar as authored, never from another fusion's binding. */
740
+ const FUSE_ORIGINAL = Symbol.for('parseman.interpretedFuseOriginal')
741
+ /** What an interpreted fuse repointed this slot at. */
742
+ const FUSE_TARGET = Symbol.for('parseman.interpretedFuseTarget')
743
+ /** The source rule maps behind a `fuseInterpreted()` result, so it can be fused
744
+ * again — the interpreted mirror of `COMPOSED_PIECES`. */
745
+ const INTERPRETED_PIECES = Symbol.for('parseman.interpretedPieces')
746
+
747
+ /** Whether `map` is an interpreted fuse (a combinator map) rather than a compiled
748
+ * `compose()` result (a map of fused functions). INTERNAL — not re-exported from
749
+ * `src/index.ts`. A consumer never has to ask this question: what it holds is
750
+ * whatever the macro built. Diagnostics that fuse interpreted on purpose do. */
751
+ export function isInterpretedFuse(map: object): boolean {
752
+ return Array.isArray((map as Record<symbol, unknown>)[INTERPRETED_PIECES])
753
+ }
754
+
755
+ type NamedEntries = ReadonlyArray<[string, Combinator<unknown>]>
756
+ /** `plain` = the item was an authored `rules()` map. Only plain maps declare the
757
+ * composing trivia, mirroring `composingTriviaOf`, which skips artifacts and prior
758
+ * composed results for exactly the same reason: their trivia was already applied. */
759
+ type FusePiece = { entries: NamedEntries; plain: boolean }
760
+
761
+ /** The rule a map entry DEFINES, or `undefined` when it is an external reference
762
+ * (an accessed-but-undefined `g.X`). Same local-vs-external test `compileLinkable`
763
+ * and `itemCarried` apply, so the two fuses agree on what a piece contributes. */
764
+ function definitionOf(entry: Combinator<unknown>): Combinator<unknown> | undefined {
765
+ if (isRefSlot(entry)) {
766
+ const original = (entry as unknown as Record<symbol, unknown>)[FUSE_ORIGINAL]
767
+ if (original !== undefined) return (original as Combinator<unknown> | null) ?? undefined
768
+ try { return entry._def.thunk() } catch { return undefined }
769
+ }
770
+ if (entry._def.tag === 'lazy') {
771
+ try { (entry._def as { thunk: () => Combinator<unknown> }).thunk() } catch { return undefined }
772
+ }
773
+ return entry
774
+ }
775
+
776
+ /** Point `slot` at `target`, updating every call site that holds it. Mirrors
777
+ * `ref().define()`'s metadata propagation; refuses to overwrite a binding a
778
+ * DIFFERENT fusion already made (see the mutation note above). */
779
+ function repointRef(slot: RefSlot, target: Combinator<unknown>, name: string): void {
780
+ const tagged = slot as unknown as Record<symbol, unknown>
781
+ const bound = tagged[FUSE_TARGET] as Combinator<unknown> | undefined
782
+ const original = FUSE_ORIGINAL in tagged
783
+ ? tagged[FUSE_ORIGINAL] as Combinator<unknown> | null
784
+ : (() => { try { return slot._def.thunk() } catch { return null } })()
785
+ if ((bound ?? original) === target) return
786
+ if (bound !== undefined) {
787
+ throw new Error(
788
+ `fuseInterpreted: rule "${name}" is already bound by a DIFFERENT interpreted fusion of the same grammar piece. `
789
+ + `An interpreted fuse binds the shared placeholder objects in place, so two fusions cannot share a piece — `
790
+ + `build a fresh instance of the piece (call its rules() factory again, or import the module under a distinct specifier) for the second fusion.`,
791
+ )
792
+ }
793
+ if (!(FUSE_ORIGINAL in tagged)) Object.defineProperty(slot, FUSE_ORIGINAL, { value: original, enumerable: false })
794
+ Object.defineProperty(slot, FUSE_TARGET, { value: target, enumerable: false })
795
+ slot._def.thunk = () => target
796
+ slot.parse = (input, pos, ctx) => target.parse(input, pos, ctx)
797
+ const meta = slot._meta
798
+ meta.firstSet = target._meta.firstSet
799
+ meta.canMatchNewline = target._meta.canMatchNewline
800
+ meta.isTrivia = target._meta.isTrivia
801
+ if (target._meta.triviaKindLabels !== undefined) meta.triviaKindLabels = target._meta.triviaKindLabels
802
+ else (meta as { triviaKindLabels: readonly string[] | undefined }).triviaKindLabels = undefined
803
+ if (target._meta.disjoint !== undefined) meta.disjoint = target._meta.disjoint
804
+ else (meta as { disjoint: boolean | undefined }).disjoint = undefined
805
+ }
806
+
807
+ /** Flatten one `fuseInterpreted()` item to the rule maps it contributes, in order. */
808
+ function interpretedPieces(item: LinkableTable | Record<string, unknown>): FusePiece[] {
809
+ const fused = (item as Record<symbol, unknown>)[INTERPRETED_PIECES]
810
+ if (Array.isArray(fused)) return (fused as NamedEntries[]).map(entries => ({ entries, plain: false }))
811
+ const carried = composedPiecesOf(item as Record<string, unknown>)
812
+ if (carried !== undefined) {
813
+ // A compiled `compose()` result. Its carried IR re-lowers to combinators, but a
814
+ // piece that arrived already COMPILED has no combinator graph at all — fusing
815
+ // around it would silently drop its rules, which is the one failure mode a
816
+ // diagnostic must never have.
817
+ const { maps, opaque } = carriedRuleMapsDetailed(carried)
818
+ if (opaque.length > 0) {
819
+ throw new Error(
820
+ `fuseInterpreted: cannot interpret a composed grammar containing precompiled artifact(s) `
821
+ + `${opaque.map(o => `"${o.ns}" (${o.ruleNames.length} rules)`).join(', ')} — they carry compiled functions, not a combinator graph. `
822
+ + `Pass the source grammars (the same items you passed to compose()) instead.`,
823
+ )
824
+ }
825
+ return maps.map(entries => ({ entries, plain: false }))
826
+ }
827
+ if (isLinkableTable(item)) {
828
+ // A table piece DOES carry its IR, so this is recoverable rather than fatal: hydrate
829
+ // the combinator graph back out of it and interpret that.
830
+ const rules = ruleMapOfCarried(item)
831
+ if (rules === undefined) {
832
+ throw new Error('fuseInterpreted: a precompiled linkable artifact with no carried IR has no combinator graph to interpret; pass the source grammar (a rules() map) instead')
833
+ }
834
+ return [{ entries: rules, plain: false }]
835
+ }
836
+ return [{ entries: Object.entries(item as Record<string, Combinator<unknown>>), plain: true }]
837
+ }
838
+
839
+ /**
840
+ * Materialize a composition as a RUNNABLE INTERPRETED rule map — the interpreted
841
+ * counterpart of `compose()`, with identical fuse semantics (later piece wins,
842
+ * override reroutes the base's own calls, composing trivia governs every rule).
843
+ * No codegen, no `new Function`, no macro build step: the result is a plain map of
844
+ * combinators that `run()` / `parseDoc()` accept exactly like a fused map.
845
+ *
846
+ * This is what diagnostics and profiling run against — they must stay in
847
+ * interpreted mode, and before this they could not see a composed grammar at all.
848
+ *
849
+ * Items are the SAME items `compose()`/`composeLeaf()` take: `rules()` maps
850
+ * (the intended input), a prior `fuseInterpreted()` result, or a runtime
851
+ * `compose()` result (re-lowered from its carried IR — note that carried IR cannot
852
+ * materialize direct `node()` builders, so prefer the source maps). A precompiled
853
+ * `linkable()` artifact is rejected: it has no combinator graph.
854
+ *
855
+ * MUTATION: binding a cross-piece hole rewrites the shared placeholder object every
856
+ * call site already holds — that IS how an override reaches a base piece's own
857
+ * calls. A second, DIFFERENT fusion over the same piece objects therefore throws
858
+ * rather than silently rewriting the first one's parser.
859
+ */
860
+ export function fuseInterpreted(
861
+ items: Array<LinkableTable | Record<string, unknown>>,
862
+ opts?: { hostMode?: HostMode },
863
+ ): Record<string, Combinator<unknown>> {
864
+ return fusePieces(items.flatMap(interpretedPieces), opts)
865
+ }
866
+
867
+ /** The rule names a fusion of these pieces defines, in winner order — computable
868
+ * WITHOUT binding anything, which is what lets `composeLeaf()` expose its key set
869
+ * before it fuses. */
870
+ function ruleNamesOf(pieces: FusePiece[]): string[] {
871
+ const names = new Set<string>()
872
+ for (const piece of pieces) {
873
+ for (const [name, value] of piece.entries) if (definitionOf(value) !== undefined) names.add(name)
874
+ }
875
+ return [...names]
876
+ }
877
+
878
+ function fusePieces(
879
+ pieces: FusePiece[],
880
+ opts?: { hostMode?: HostMode },
881
+ ): Record<string, Combinator<unknown>> {
882
+ // Composing-wins trivia, read exactly as `composingTriviaOf` reads it for compose():
883
+ // the LAST authored grammar that declared `rules({ trivia }, …)`.
884
+ let trivia: Combinator<unknown> | undefined
885
+ for (let i = pieces.length - 1; i >= 0 && trivia === undefined; i--) {
886
+ if (!pieces[i]!.plain) continue
887
+ for (const [, rule] of pieces[i]!.entries) {
888
+ const t = rule._meta.grammarTrivia
889
+ if (t) { trivia = t; break }
890
+ }
891
+ }
892
+
893
+ // Winner per rule name — later piece wins, matching `fuseRules`. The winner is the
894
+ // DEFINITION, never the slot holding it: a slot can be repointed, and a chain
895
+ // through one would make an override of X reroute into itself.
896
+ const winner = new Map<string, Combinator<unknown>>()
897
+ const entry = new Map<string, Combinator<unknown>>()
898
+ for (const piece of pieces) {
899
+ for (const [name, value] of piece.entries) {
900
+ const def = definitionOf(value)
901
+ if (def === undefined) continue
902
+ winner.set(name, def)
903
+ entry.set(name, value)
904
+ }
905
+ }
906
+
907
+ // Bind every hole (and repoint every overridden slot) before anything runs.
908
+ const missing = new Set<string>()
909
+ for (const piece of pieces) {
910
+ for (const [name, value] of piece.entries) {
911
+ if (!isRefSlot(value)) continue
912
+ const target = winner.get(name)
913
+ if (target === undefined) { missing.add(name); continue }
914
+ repointRef(value, target, name)
915
+ }
916
+ }
917
+ if (missing.size > 0) throw new Error(missingRuleMessage(pieces, missing))
918
+
919
+ const out: Record<string, Combinator<unknown>> = {}
920
+ for (const [name, value] of entry) {
921
+ out[name] = value
922
+ const meta = value._meta as {
923
+ isTrivia: boolean
924
+ grammarTrivia?: Combinator<unknown>
925
+ grammarHostMode?: HostMode
926
+ }
927
+ // A trivia rule must never carry the ambient trivia (it would recursively skip
928
+ // trivia within itself) — the same guard `compileLinkable` applies per rule.
929
+ if (trivia !== undefined && !meta.isTrivia) meta.grammarTrivia = trivia
930
+ // Host mode is PER PIECE, exactly as `compileLinkable` resolves it: an explicit
931
+ // option wins, otherwise the owning piece's own `rules({ hostMode })` stamp.
932
+ if (opts?.hostMode !== undefined && !meta.isTrivia) {
933
+ if (opts.hostMode === 'cst') meta.grammarHostMode = 'cst'
934
+ else (meta as { grammarHostMode: HostMode | undefined }).grammarHostMode = undefined
935
+ }
936
+ }
937
+ Object.defineProperty(out, INTERPRETED_PIECES, {
938
+ value: pieces.filter(p => p.plain).map(p => p.entries),
939
+ enumerable: false,
940
+ })
941
+ return out
942
+ }
943
+
944
+ /** Name-closure failure, reported the way the compiled fuse reports it: which rule
945
+ * referenced the missing name. Computed only on the error path. */
946
+ function missingRuleMessage(pieces: FusePiece[], missing: Set<string>): string {
947
+ for (const piece of pieces) {
948
+ const deps = ruleDependencies(piece.entries.filter(([, v]) => definitionOf(v) !== undefined))
949
+ for (const [name, ds] of deps) {
950
+ for (const d of ds) if (missing.has(d)) return `fuseInterpreted: rule "${name}" references missing rule "${d}"`
951
+ }
952
+ }
953
+ return `fuseInterpreted: missing rule(s) ${[...missing].map(n => `"${n}"`).join(', ')}`
954
+ }