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,45 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+
3
+ /**
4
+ * Runs `combinator` with `ctx.state` set to `extra` for the duration of the parse.
5
+ * The outer user context is restored on exit (lexical scoping).
6
+ *
7
+ * const functionBody = withCtx({ inFn: true },
8
+ * sequence(literal('{'), many(statement), literal('}'))
9
+ * )
10
+ *
11
+ * Read back with guard() or from within a transform's span argument.
12
+ */
13
+ export function withCtx<U, T>(extra: U, combinator: Combinator<T>): Combinator<T> {
14
+ const meta: ParserMeta = {
15
+ firstSet: combinator._meta.firstSet,
16
+ canMatchNewline: combinator._meta.canMatchNewline,
17
+ isTrivia: false,
18
+ }
19
+ return {
20
+ _tag: 'withCtx',
21
+ _meta: meta,
22
+ _def: { tag: 'withCtx', extra, parser: combinator as Combinator<unknown> },
23
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
24
+ // SAVE / RESTORE, not a clone. This used to run the child against
25
+ // `{ ...ctx, state: extra }`, which scopes far more than the state: every
26
+ // scalar the child writes on `ctx` lands on the clone and dies with it —
27
+ // including `_fe` / `_fx`, the furthest-failure position and its expected
28
+ // set. A failing `withCtx` subtree therefore contributed NOTHING to the
29
+ // parent's expected set, silently. Nobody asked for that isolation; the
30
+ // combinator's contract is "set `state` for the duration", and the clone
31
+ // was an implementation detail with a semantic reach nobody consented to.
32
+ //
33
+ // (Array MUTATIONS were never affected either way — `_triviaLog` and the
34
+ // CST buffers are the same objects through a spread — so this changes the
35
+ // scalars only, which is exactly the part that was being dropped.)
36
+ //
37
+ // It also allocated a full context per entry, on the object every
38
+ // combinator reads on every step.
39
+ const saved = ctx.state
40
+ ctx.state = extra
41
+ try { return combinator.parse(input, pos, ctx) }
42
+ finally { ctx.state = saved }
43
+ },
44
+ }
45
+ }
@@ -0,0 +1,330 @@
1
+ import type { ParserDef } from '../types.ts'
2
+ import { recordDegradation } from './degradation.ts'
3
+
4
+ /**
5
+ * Per-node children/fields/raw/trivia/state capture is dead work when the build
6
+ * never declares the corresponding formal param. This module derives a build's
7
+ * *confirmed* formal-parameter arity so the compiler and macro can elide their
8
+ * direct-AST-only CST collectors without changing structural/CST output.
9
+ *
10
+ * Conservative by construction: any source we can't confidently parse — rest params,
11
+ * destructuring, `arguments`, an unrecognized shape — yields `null` (arity unknown),
12
+ * and callers KEEP full capture. We only ever return a number when the parameter list
13
+ * is a plain comma-separated list of simple identifiers.
14
+ */
15
+
16
+ type NodeDef = Extract<ParserDef, { tag: 'node' }>
17
+
18
+ /**
19
+ * Is a positioned-CST host installed for this parse?
20
+ *
21
+ * `cstBuildHost` (and the language-service hosts) mark themselves with
22
+ * `_parsemanCstOutput`. That flag is what re-routes a node with its OWN direct builder
23
+ * through the host — and therefore what makes the direct builder's formal arity the
24
+ * WRONG basis for eliding capture, since the consumer is no longer that builder.
25
+ *
26
+ * The COMPILED engine answers this at compile time (`hostMode`, 0.37.0). The interpreter
27
+ * has no compile step, so it asks per parse.
28
+ */
29
+ export function cstOutputHost(build: unknown): boolean {
30
+ return (build as { _parsemanCstOutput?: true } | undefined)?._parsemanCstOutput === true
31
+ }
32
+
33
+ /**
34
+ * ── THE ONE RULE IN THIS FILE ────────────────────────────────────────────────────
35
+ * A WRONG NUMBER IS THE BUG. `null` (unknown) IS ALWAYS ACCEPTABLE.
36
+ *
37
+ * `null` fails open: the caller keeps every capture tier and records a degradation, so
38
+ * the output is correct and the cost is visible. A wrong NUMBER silently drops capture
39
+ * tiers — fields vanish from the AST with no diagnostic, because a confident answer
40
+ * never reaches `recordDegradation`. Every branch below that cannot decide with
41
+ * certainty must therefore return `null`, never a guess.
42
+ * ─────────────────────────────────────────────────────────────────────────────────
43
+ */
44
+
45
+ /** `x => ...` — a single unparenthesised identifier parameter. */
46
+ const SINGLE_IDENT_ARROW_RE = /^[A-Za-z_$][\w$]*\s*=>/
47
+
48
+ /** Start of a formal-parameter list: `(`, or `function name?(`. */
49
+ const PARAM_LIST_OPEN_RE = /^(?:function\b[^(]*)?\(/
50
+
51
+ /**
52
+ * A host builtin, a `Function.prototype.bind` result and a `Proxy` all stringify to
53
+ * `function () { [native code] }`. That has an EMPTY parameter list which says nothing
54
+ * whatever about the underlying function's arity — reading it as arity 0 is a confident
55
+ * lie that silently drops every capture tier. None of those are documented reducer
56
+ * shapes and parseman does not owe them support; it owes them an honest `null`.
57
+ */
58
+ const NATIVE_CODE_RE = /\{\s*\[native code\]\s*\}/
59
+
60
+ /** Identifier head of a formal param, with its optional `?`. */
61
+ const PARAM_HEAD_RE = /^[A-Za-z_$][\w$]*\s*\??\s*/
62
+
63
+ /**
64
+ * Confirmed count of simple formal parameters the build declares, or `null` when the
65
+ * source can't be parsed into a plain identifier list (→ caller keeps full capture).
66
+ */
67
+ export function confirmedBuildArity(src: string): number | null {
68
+ const s = src.trim()
69
+
70
+ // Unreadable source → unknown. Checked FIRST: its parameter list is empty, and the
71
+ // empty-list answer below would otherwise report a confident 0.
72
+ if (NATIVE_CODE_RE.test(s)) return null
73
+
74
+ // A stray `arguments` reference defeats formal-arity reasoning entirely; only a
75
+ // full-source scan can rule it out. Cheap guard against the obvious cases.
76
+ //
77
+ // Checked BEFORE the empty-parameter-list answer below. It used to sit after it, which
78
+ // made it unreachable for exactly the shape it matters most for: `function () { return
79
+ // arguments[0] }` has an empty formal list and real arity ≥ 1, and was reported 0.
80
+ if (/\barguments\b/.test(s)) return null
81
+
82
+ // Single-ident arrow: `x => ...` → exactly one param.
83
+ if (SINGLE_IDENT_ARROW_RE.test(s)) return 1
84
+
85
+ const inner = paramListText(s)
86
+ if (inner === null) return null
87
+ if (inner.trim() === '') return 0
88
+
89
+ const parts = splitTopLevel(inner)
90
+ if (parts === null) return null
91
+
92
+ // Reject anything that isn't a flat list of simple identifiers. A rest param
93
+ // (`...args`), destructuring (`{a}`, `[a]`), or default (`a = 1`) all appear here.
94
+ for (const part of parts) {
95
+ if (!isConfirmableParam(part.trim())) return null
96
+ }
97
+
98
+ return parts.length
99
+ }
100
+
101
+ /**
102
+ * The text between a build's parameter parentheses, or `null` when it cannot be located
103
+ * with certainty.
104
+ *
105
+ * Found by a BALANCED scan rather than a character class. The previous `[^)]*` stopped at
106
+ * the first `)`, which in ordinary TypeScript is the one inside a function-typed
107
+ * annotation: `(children: (n: N) => N, fields) => …` yielded `children: (n: N` and
108
+ * reported arity 1 instead of 2 — a silent under-capture on precisely the typed shape the
109
+ * docs tell authors to write.
110
+ */
111
+ function paramListText(s: string): string | null {
112
+ const open = PARAM_LIST_OPEN_RE.exec(s)
113
+ if (!open) return null
114
+ const start = open[0].length
115
+ const end = matchingParen(s, start)
116
+ if (end === null) return null
117
+ // An arrow's list must be followed by `=>`; a `function`'s is followed by its body.
118
+ // Without this a parenthesised expression could be read as a parameter list.
119
+ if (!s.startsWith('function') && !/^\s*=>/.test(s.slice(end + 1))) return null
120
+ return s.slice(start, end)
121
+ }
122
+
123
+ /** Index of the `)` closing the list opened just before `start`, else `null`. */
124
+ function matchingParen(s: string, start: number): number | null {
125
+ let depth = 0
126
+ for (let i = start; i < s.length; i++) {
127
+ const c = s[i]!
128
+ if (c === '"' || c === "'" || c === '`') {
129
+ const close = skipString(s, i)
130
+ if (close === null) return null
131
+ i = close
132
+ continue
133
+ }
134
+ if (c === '(' || c === '[' || c === '{') depth++
135
+ else if (c === ']' || c === '}') { if (--depth < 0) return null }
136
+ else if (c === ')') { if (depth === 0) return i; depth-- }
137
+ }
138
+ return null
139
+ }
140
+
141
+ /** Index of the closing quote of the literal opened at `open`, else `null`. */
142
+ function skipString(s: string, open: number): number | null {
143
+ const quote = s[open]!
144
+ for (let i = open + 1; i < s.length; i++) {
145
+ if (s[i] === '\\') { i++; continue }
146
+ if (s[i] === quote) return i
147
+ }
148
+ return null
149
+ }
150
+
151
+ /**
152
+ * Split a parameter list on TOP-LEVEL commas, or `null` if it cannot be split with
153
+ * certainty.
154
+ *
155
+ * Tracks `()`/`[]`/`{}` AND `<>`, because a comma inside type arguments belongs to the
156
+ * annotation, not the list: a plain `,`-split turned `(a: Map<K, V>, b) => …` into three
157
+ * fragments. `=>` is consumed as a unit so the `>` of an arrow type is not mistaken for
158
+ * the close of a type-argument list.
159
+ */
160
+ function splitTopLevel(inner: string): string[] | null {
161
+ const parts: string[] = []
162
+ let depth = 0
163
+ let angle = 0
164
+ let last = 0
165
+ for (let i = 0; i < inner.length; i++) {
166
+ const c = inner[i]!
167
+ if (c === '=' && inner[i + 1] === '>') { i++; continue }
168
+ if (c === '"' || c === "'" || c === '`') {
169
+ const close = skipString(inner, i)
170
+ if (close === null) return null
171
+ i = close
172
+ continue
173
+ }
174
+ if (c === '(' || c === '[' || c === '{') depth++
175
+ else if (c === ')' || c === ']' || c === '}') { if (--depth < 0) return null }
176
+ else if (c === '<') angle++
177
+ else if (c === '>') { if (--angle < 0) return null }
178
+ else if (c === ',' && depth === 0 && angle === 0) { parts.push(inner.slice(last, i)); last = i + 1 }
179
+ }
180
+ if (depth !== 0 || angle !== 0) return null
181
+ parts.push(inner.slice(last))
182
+ return parts
183
+ }
184
+
185
+ /**
186
+ * A confirmable formal param: a plain identifier, optionally `?`-optional and optionally
187
+ * carrying a TypeScript type annotation (`c`, `c?`, `c: any`, `c?: Map<K, V>`, `c: (n: N)
188
+ * => N`). The build source is sliced verbatim from the (possibly TS) grammar source, so
189
+ * typed params must be recognized or every typed grammar keeps full capture.
190
+ *
191
+ * A DEFAULT (`a = 1`, `a: number = 1`) stays unconfirmed: the parameter is optional, so
192
+ * the formal count overstates what the reducer is guaranteed to receive.
193
+ */
194
+ function isConfirmableParam(part: string): boolean {
195
+ const head = PARAM_HEAD_RE.exec(part)
196
+ if (!head) return false // rest `...a`, destructuring `{a}` / `[a]`
197
+ const rest = part.slice(head[0].length)
198
+ if (rest === '') return true
199
+ if (rest[0] !== ':') return false // `a = 1`, or anything else we cannot read
200
+ return !hasDefaultAssign(rest.slice(1))
201
+ }
202
+
203
+ /** An `=` that is not part of `=>`, `>=`, `<=`, `==` or `!=` — i.e. a default value. */
204
+ function hasDefaultAssign(annotation: string): boolean {
205
+ for (let i = 0; i < annotation.length; i++) {
206
+ if (annotation[i] !== '=') continue
207
+ if (annotation[i + 1] === '>' || annotation[i + 1] === '=') { i++; continue }
208
+ const prev = annotation[i - 1]
209
+ if (prev === '<' || prev === '>' || prev === '!' || prev === '=') continue
210
+ return true
211
+ }
212
+ return false
213
+ }
214
+
215
+ /**
216
+ * The source a def's arity/shape analysis must read.
217
+ *
218
+ * `buildSrc` is the source text of the EXPRESSION written at the `node(...)` call
219
+ * site. When the reducer is passed as a bare identifier — `node('Foo', p, { build:
220
+ * foldOperation })` — that text is just `"foldOperation"`, which matches no parameter
221
+ * list and used to fail open into all five capture tiers, silently. The macro plugin
222
+ * already holds the whole module's AST, so it resolves such an identifier to its
223
+ * module-scope declaration and parks that declaration's source here. `buildSigSrc` is
224
+ * ANALYSIS-ONLY: it is never emitted, so the generated builder reference is unchanged.
225
+ */
226
+ export function buildAnalysisSrc(def: NodeDef): string {
227
+ return def.buildSigSrc ?? def.buildSrc ?? def.build!.toString()
228
+ }
229
+
230
+ /**
231
+ * Why a parameter list could not be read — the actionable half of the diagnostic.
232
+ *
233
+ * Only genuinely undecidable shapes reach here. The resolver (`plugin/reducer-resolver.
234
+ * ts`) handles named reducers, cross-module imports, namespace members, aliases,
235
+ * non-reassigned `let`/`var`, defaults and destructuring; a decline from it carries a
236
+ * machine-readable reason, which is what the author needs in order to fix or declare it.
237
+ */
238
+ const RESOLVER_REASONS: Record<string, string> = {
239
+ 'rest-parameter': 'its parameter list uses a rest parameter, so the declared arity is unbounded',
240
+ 'arguments': 'its body references `arguments`',
241
+ 'reassigned': 'the binding it names is reassigned, so the function read here is not decidable',
242
+ 'not-a-function': 'the binding it names is not a function declaration',
243
+ 'unresolved-import': 'its module could not be resolved or parsed',
244
+ 'not-found': 'the name did not resolve to any binding',
245
+ 'computed': 'it is a computed expression',
246
+ 'foreign-source': 'it is named in a module the resolver was never told about, so its '
247
+ + 'offsets index no scope tree the resolver holds',
248
+ 'ambiguous-source': 'two distinct registered modules hold byte-identical text, so that '
249
+ + 'text no longer names the module its offsets index',
250
+ }
251
+
252
+ function unconfirmableReason(def: NodeDef, src: string): string {
253
+ const declared = def.buildArityUnresolved
254
+ if (declared !== undefined && RESOLVER_REASONS[declared]) return RESOLVER_REASONS[declared]!
255
+ const s = src.trim()
256
+ const params = s.slice(0, s.indexOf(')') + 1)
257
+ if (params.includes('...')) return RESOLVER_REASONS['rest-parameter']!
258
+ if (/\barguments\b/.test(s)) return RESOLVER_REASONS['arguments']!
259
+ if (/^[A-Za-z_$][\w$]*(\s*\.\s*[A-Za-z_$][\w$]*)?$/.test(s)) {
260
+ return 'it names a binding this build could not resolve to a function declaration'
261
+ }
262
+ return 'its parameter list could not be parsed'
263
+ }
264
+
265
+ /**
266
+ * Confirmed formal arity for a node def, reporting once when it cannot be confirmed.
267
+ *
268
+ * Order of authority:
269
+ * 1. `node(..., { buildArity })` — the author DECLARED it. Nothing overrides that.
270
+ * 2. `def.buildArity` as resolved by the macro's reducer resolver (real scope analysis
271
+ * and cross-module import following).
272
+ * 3. Reading the parameter list out of whatever source we have.
273
+ *
274
+ * Fail-open is the correct BEHAVIOR — capturing too much is safe, capturing too little
275
+ * is a correctness bug. Fail-open with no diagnostic was the defect. But a diagnostic is
276
+ * the right answer only for the genuinely undecidable; for everything a static analysis
277
+ * can decide, the right answer is to decide it, which is why (2) exists and why (1) gives
278
+ * the author a way out of (3) entirely.
279
+ */
280
+ export function confirmedArityForDef(def: NodeDef): number | null {
281
+ if (def.buildArity !== undefined) return def.buildArity
282
+ const src = buildAnalysisSrc(def)
283
+ const arity = confirmedBuildArity(src)
284
+ if (arity !== null) return arity
285
+ const subject = `build reducer \`${(def.buildSrc ?? '<runtime fn>').trim().split('\n')[0]!.slice(0, 60)}\``
286
+ recordDegradation({
287
+ code: 'build-arity-unconfirmed',
288
+ severity: 'warn',
289
+ where: `node("${def.type ?? '<inferred>'}")`,
290
+ subject,
291
+ fellBackTo: `could not confirm its declared arity — ${unconfirmableReason(def, src)} — `
292
+ + 'so this node captures children, fields and raw children, logs trivia, and clones `_ctx.state` on every match',
293
+ otherwise: 'only the tiers the reducer actually declares would be captured '
294
+ + '(arity >= 1 children, >= 2 fields, >= 4 raw, >= 5 trivia, >= 6 state). '
295
+ + 'Declare it with `node(..., { buildArity: n })` if it cannot be derived',
296
+ })
297
+ return null
298
+ }
299
+
300
+ /** Build reads its 1st (`children`) arg? Unknown/unparseable → true (keep capture). */
301
+ export function buildReadsChildren(def: NodeDef): boolean {
302
+ if (!def.build) return true // structural node: CST output always owns children
303
+ const arity = confirmedArityForDef(def)
304
+ if (arity === null) return true
305
+ return arity >= 1
306
+ }
307
+
308
+ /** Build reads its 4th (`rawChildren`) arg? Unknown/unparseable → true (keep capture). */
309
+ export function buildReadsRaw(def: NodeDef): boolean {
310
+ if (!def.build) return true // structural node: host/default CST may read raw children
311
+ const arity = confirmedArityForDef(def)
312
+ if (arity === null) return true
313
+ return arity >= 4
314
+ }
315
+
316
+ /** Build reads the 5th (triviaLog) arg? Unknown/unparseable → true (keep capture). */
317
+ export function buildReadsTrivia(def: NodeDef): boolean {
318
+ if (!def.build) return true // structural node: host may read trivia — keep capture
319
+ const arity = confirmedArityForDef(def)
320
+ if (arity === null) return true
321
+ return arity >= 5
322
+ }
323
+
324
+ /** Build reads the 6th (state) arg? Unknown/unparseable → true (keep state clone). */
325
+ export function buildReadsState(def: NodeDef): boolean {
326
+ if (!def.build) return true // structural node: host may read state — keep clone
327
+ const arity = confirmedArityForDef(def)
328
+ if (arity === null) return true
329
+ return arity >= 6
330
+ }
@@ -0,0 +1,209 @@
1
+ import type { Combinator } from '../types.ts'
2
+ import type { HostMode } from '../cst/host-mode.ts'
3
+ import { childrenOf } from '../analysis/gating.ts'
4
+ import { collectGrammarReflection, type GrammarReflection } from '../cst/reflection.ts'
5
+ import { classifyRuleMap } from '../analysis/commitment.ts'
6
+ import { serializeRuleMap } from './ir-serialize.ts'
7
+ import { compileRuleMap, compileRuleMapRunnable, type TableRuleMapOptions } from '../table/compile-rule-map.ts'
8
+ import type { TableProgram, TableRule } from '../table/program.ts'
9
+ import { PARSEMAN_VERSION } from '../version.ts'
10
+
11
+ /**
12
+ * `compileLinkable()` FOR THE TABLE LOWERING — `linkable()` produces TABLES too.
13
+ *
14
+ * ── WHAT `compileLinkable` ACTUALLY RETURNS, AND WHY NONE OF IT TRANSFERS ─────
15
+ * `LinkablePieces` is `prelude` / `ruleFns` / `wrappers` / `firstSets` /
16
+ * `firstSetRecipes` / `nullable` / `deps` / `needsEmptyTl` — every field of it is
17
+ * SOURCE TEXT or an input to resolving source text. It exists because
18
+ * source-lowering composition is a TEXTUAL splice: `fusedBody()` concatenates
19
+ * namespaced preludes, picks a winning `_r_<Name>` per name, and substitutes
20
+ * `@FS:` dispatch placeholders with the winner's first-set condition. A table has
21
+ * no text to splice and no placeholders to resolve, so porting those fields would
22
+ * be porting the mechanism rather than the capability.
23
+ *
24
+ * The capability is: a piece must be RUNNABLE on its own, and COMPOSABLE with
25
+ * other pieces. This artifact carries exactly those two things.
26
+ *
27
+ * `prog` / `rules` — the piece AS A TABLE. Present whenever the piece is
28
+ * self-contained. This is the ruling: a `linkable()`
29
+ * artifact is a table, not a bag of compiled functions.
30
+ * `ir` — the piece's combinator graph, serialized. This is the
31
+ * composable half, and it is NOT new machinery: the plugin
32
+ * already carries pieces as IR (`plugin/index.ts:1772`) and
33
+ * the linker already re-lowers them (`linker.ts:640`,
34
+ * `compileLinkable(evalRuleMapIR(p.ir), …)`).
35
+ *
36
+ * ── WHY THAT MAKES COMPOSITION THE EASY KIND ─────────────────────────────────
37
+ * The open question was whether table-to-table composition has to merge two
38
+ * ALREADY-ENCODED programs — relocating code offsets and merging const, fn,
39
+ * class, expected and dispatch pools. It does not, as long as every piece
40
+ * carries its IR: the composer evaluates each piece's IR back to a rule map,
41
+ * merges the maps (later pieces overriding earlier names, which is what
42
+ * `compose()` means), and calls `encodeTable` ONCE over the merged map. One
43
+ * encode, no relocation, and the result is byte-for-byte the table the merged
44
+ * grammar would have produced if it had been written as one `rules()` call.
45
+ *
46
+ * The relocation route is only forced for a piece with NO IR, which is why `ir`
47
+ * being null is reported here as a first-class fact rather than folded into a
48
+ * bare `null` return.
49
+ *
50
+ * ── THE HOLE CASE IS NOT REFUSED ─────────────────────────────────────────────
51
+ * A piece that references a rule it does not define (`g.Value` with no
52
+ * `.define()` — `hasExternalRuleRef`'s SHARED-SHAPE signature) cannot be encoded
53
+ * standalone: `encodeTable` resolves a `lazy` by calling its thunk, and that
54
+ * thunk throws. Such a piece gets `prog: null` and `rules: null` and keeps its
55
+ * `ir`, which is the correct answer rather than a refusal: a shape with a hole
56
+ * is not a parser in ANY lowering, and it composes through the merge above,
57
+ * where the hole is filled by whichever piece supplies the name.
58
+ */
59
+ export type LinkableTableOptions = Omit<TableRuleMapOptions, 'runtimeRef'>
60
+
61
+ export type LinkableTable = {
62
+ /**
63
+ * The parseman version that produced this artifact — the same ARTIFACT VERSION
64
+ * LOCK `LinkablePieces.v` carries, for the same reason: artifacts are
65
+ * version-locked and the format has no back-compat read path.
66
+ */
67
+ v: string
68
+ ns: string
69
+ /** Local rule names, external (undefined) entries already dropped. */
70
+ keys: string[]
71
+ /** Rule names this piece REFERENCES but does not define. */
72
+ external: string[]
73
+ /** The piece as a table — null when it has holes (see `external`). */
74
+ prog: TableProgram | null
75
+ /** The runnable form of `prog` — null for the same reason. */
76
+ rules: Record<string, TableRule> | null
77
+ /** The emitted expression for `prog`, or null when there is no `prog`. */
78
+ replacement: string | null
79
+ /**
80
+ * The piece's combinator graph, serialized — the COMPOSABLE half. Null when
81
+ * the map cannot be faithfully serialized (a callback with no captured
82
+ * source); a null here is what would force composition to merge encoded
83
+ * programs instead of merging rule maps.
84
+ */
85
+ ir: string | null
86
+ /**
87
+ * The piece's rule map as LIVE COMBINATORS — present only for an in-process
88
+ * artifact, never serialized.
89
+ *
90
+ * `ir` is the portable composable half, and a grammar BUILT AT RUNTIME often has no
91
+ * `ir` at all (a live callback has no recoverable source). Such a piece still has to
92
+ * compose, and in-process it can: the combinators themselves are right there. Without
93
+ * this the runtime `compose([linkable(g)])` refused a grammar the source lowering
94
+ * composed happily.
95
+ */
96
+ ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>
97
+ hostMode: HostMode
98
+ hostBranchElided: boolean
99
+ /**
100
+ * Whether any rule carries a DIRECT builder, and whether the piece is free of
101
+ * semantic reduction. `composeLeaf` gates on both, and they are predicates over the
102
+ * combinator graph rather than products of lowering it — so they belong on the
103
+ * artifact regardless of which lowering produced it.
104
+ */
105
+ hasDirectBuilders: boolean
106
+ isRecognitionOnly: boolean
107
+ reflection: GrammarReflection
108
+ }
109
+
110
+ /**
111
+ * Local vs external, decided exactly as `compileLinkable` decides it: a
112
+ * `rules(g => …)` cache also holds every `g.X` that was merely ACCESSED, so an
113
+ * accessed-but-undefined rule leaks into `Object.entries` as an unresolvable
114
+ * lazy. Those are not local rules.
115
+ */
116
+ function isLocal(rule: Combinator<unknown>): boolean {
117
+ const d = rule._def
118
+ if (d.tag !== 'lazy') return true
119
+ try { d.thunk(); return true } catch { return false }
120
+ }
121
+
122
+ /** Every named rule this map references but does not define, in first-seen order. */
123
+ function externalNames(
124
+ ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
125
+ ): string[] {
126
+ const out: string[] = []
127
+ const seen = new Set<Combinator<unknown>>()
128
+ const visit = (p: Combinator<unknown>): void => {
129
+ if (seen.has(p)) return
130
+ seen.add(p)
131
+ const d = p._def
132
+ if (d.tag === 'lazy') {
133
+ let resolved: Combinator<unknown> | undefined
134
+ try { resolved = d.thunk() } catch { resolved = undefined }
135
+ if (resolved === undefined) {
136
+ const name = (p as unknown as { _ruleName?: string })._ruleName
137
+ if (name !== undefined && !out.includes(name)) out.push(name)
138
+ return
139
+ }
140
+ visit(resolved)
141
+ return
142
+ }
143
+ for (const child of childrenOf(d)) visit(child)
144
+ }
145
+ for (const [, rule] of ruleMap) visit(rule)
146
+ return out
147
+ }
148
+
149
+ export function compileLinkableTable(
150
+ ruleMapArg: ReadonlyArray<readonly [string, Combinator<unknown>]>,
151
+ ns: string,
152
+ opts: LinkableTableOptions = {},
153
+ ): LinkableTable | null {
154
+ if (!ns) throw new Error('compileLinkableTable: ns must be a non-empty namespace')
155
+ const ruleMap = ruleMapArg.filter(([, rule]) => isLocal(rule))
156
+ if (ruleMap.length === 0) return null
157
+ const external = externalNames(ruleMap)
158
+ // The duplication diagnostic is OPT-IN here, unlike at the owning `rules()` site: the
159
+ // macro lowers the same map through both, so letting `PARSEMAN_DUPLICATION` alone
160
+ // reach this call would repeat every finding the owning site already reported. An
161
+ // explicit option still runs. `'off'` is passed rather than omitted because omitting
162
+ // it is what resolves to the env var.
163
+ const dedupOpts = { ...opts, duplication: opts.duplication ?? ('off' as const) }
164
+ const compiled = external.length === 0 ? compileRuleMap(ruleMap, dedupOpts) : null
165
+ // Serialized BEFORE anything else needs it and independently of whether the
166
+ // piece encoded: a shape with a hole is precisely the case that has no table
167
+ // and must still compose.
168
+ const ir = serializeRuleMap(ruleMap, opts.scanSkip ?? null)
169
+ // RUNNABLE IS NOT PRINTABLE, and `linkable()` only needs the first.
170
+ //
171
+ // `compileRuleMap` refuses a map whose reducers have no captured SOURCE, because
172
+ // printing one would emit `() => {}` and silently return the wrong tree. That gate is
173
+ // right for the macro, which prints — and wrong here: a grammar BUILT AT RUNTIME (every
174
+ // interpreter-path `linkable()` caller) has live callbacks and no sources by
175
+ // construction, and it is never printed. Treating "cannot print" as "cannot compile"
176
+ // made `linkable()` reject the ordinary runtime grammar the source lowering accepted.
177
+ //
178
+ // So: encode again for RUNNING only. `prog` still holds the live callbacks in its
179
+ // pool, `tableRules` binds them, and `replacement` stays null — which is the
180
+ // artifact honestly saying it cannot be emitted as source.
181
+ const runnable = compiled === null && external.length === 0
182
+ ? compileRuleMapRunnable(ruleMap, dedupOpts)
183
+ : null
184
+ // NO REFUSAL PAST THIS POINT. A piece with neither a table nor IR is still USABLE
185
+ // in-process, because `ruleMap` carries the live combinators — which is exactly the
186
+ // case a grammar with holes AND unserializable callbacks lands in. Refusing it here
187
+ // made `compose()` throw for a grammar the interpreter fuses happily; the fields below
188
+ // already state, individually, what this piece can and cannot do.
189
+
190
+ const hostMode = compiled?.hostMode
191
+ ?? opts.hostMode
192
+ ?? ruleMap.map(([, r]) => r._meta.grammarHostMode).find(Boolean)
193
+ ?? 'ast'
194
+ return {
195
+ v: PARSEMAN_VERSION,
196
+ ns,
197
+ keys: ruleMap.map(([key]) => key),
198
+ external,
199
+ prog: compiled?.prog ?? runnable?.prog ?? null,
200
+ rules: compiled?.rules ?? runnable?.rules ?? null,
201
+ replacement: compiled?.replacement ?? null,
202
+ ir,
203
+ ruleMap,
204
+ hostMode,
205
+ hostBranchElided: hostMode === 'ast',
206
+ ...classifyRuleMap(ruleMap),
207
+ reflection: compiled?.reflection ?? collectGrammarReflection(ruleMap),
208
+ }
209
+ }