parseman 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +9929 -7465
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +9929 -7465
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10865 -8535
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10865 -8535
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +40 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9224 -7203
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9219 -7198
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +311 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +311 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11146 -9593
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11146 -9593
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2898 -1623
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2898 -1623
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11375 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11352 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +446 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +493 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +25 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +322 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +221 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +821 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +2994 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1766 -0
  286. package/src/table/emit.ts +418 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1672 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +459 -0
  295. package/src/table/program.ts +855 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,2281 @@
1
+ /**
2
+ * parseman unplugin — macro transform
3
+ *
4
+ * Handles: import { ... } from 'parseman' with { type: 'macro' }
5
+ *
6
+ * For each such import, walks the file's AST, finds variable declarations
7
+ * whose RHS is a pure parseman combinator call, evaluates them at build time,
8
+ * compiles the result to an optimized inline function, and replaces the
9
+ * declaration — removing the import entirely.
10
+ *
11
+ * Usage:
12
+ * // vite.config.ts
13
+ * import parseman from 'parseman/plugin'
14
+ * export default { plugins: [parseman()] }
15
+ *
16
+ * // rollup.config.js
17
+ * import parseman from 'parseman/plugin'
18
+ * export default { plugins: [parseman.rollup()] }
19
+ */
20
+ import * as fs from 'node:fs'
21
+ import * as path from 'node:path'
22
+ import { createUnplugin } from 'unplugin'
23
+ import { parseSync } from 'oxc-parser'
24
+ import { ResolverFactory } from 'oxc-resolver'
25
+ import MagicString from 'magic-string'
26
+ import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, propName, type Scope, type ScopeEntry } from './evaluator.ts'
27
+ import { classifyRuleMap } from '../analysis/commitment.ts'
28
+ import { compile } from '../table/compile.ts'
29
+ import { compileRuleMap } from '../table/compile-rule-map.ts'
30
+ import { compileLinkableTable } from '../compiler/compile-linkable-table.ts'
31
+ import { createReducerResolver } from './reducer-resolver.ts'
32
+ import { findFreeIdentifiers } from './free-identifiers.ts'
33
+ import {
34
+ beginDegradationCapture, endDegradationCapture, formatDegradation, formatDegradations,
35
+ resolveDegradationLevel, recordDegradation, degradationCaptureDepth, unwindDegradationCapture,
36
+ } from '../compiler/degradation.ts'
37
+ import type { HostMode } from '../cst/host-mode.ts'
38
+ import { COMPOSED_PIECES } from '../compiler/linker.ts'
39
+ import { evalRuleMapIR, serializeRuleMap } from '../compiler/ir-serialize.ts'
40
+ import { PARSEMAN_VERSION } from '../version.ts'
41
+ import { grammarReflectionSource, type GrammarReflection } from '../cst/reflection.ts'
42
+ import { createHash } from 'node:crypto'
43
+ import type { Combinator } from '../types.ts'
44
+ import type {
45
+ ImportDeclaration,
46
+ VariableDeclarator,
47
+ VariableDeclaration,
48
+ Expression,
49
+ Statement,
50
+ ExportNamedDeclaration,
51
+ } from '@oxc-project/types'
52
+
53
+ /**
54
+ * Where a lowered module imports the shared table driver from.
55
+ *
56
+ * The PUBLIC subpath, not a deep path into `src/`: this string is written into a
57
+ * consumer's build output, so it has to be a specifier their resolver can see.
58
+ * `parseman/table` is declared in package.json `exports` and re-exports
59
+ * `tableRules` as `tableRules` (src/table/index.ts:28).
60
+ */
61
+ const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
62
+
63
+ export type ParsecraftPluginOptions = {
64
+ /** Extra module specifiers to treat as parseman re-exports */
65
+ moduleAliases?: string[]
66
+ /**
67
+ * Warn when a regex terminal can't LOWER to a fast `charCodeAt` scan and falls
68
+ * back to `RegExp.exec`. **Default `false` (opt-in).** `RegExp.exec` is an
69
+ * accepted, JIT-fast compiled path — most un-lowered regexes are perfectly fine
70
+ * and lowering them often shows no real gain — so this is a diagnostic you turn
71
+ * ON when specifically auditing lowering coverage, not a build-time nag.
72
+ */
73
+ warnUnloweredRegex?: boolean
74
+ /**
75
+ * Bake automatic list recovery into the macro-compiled output. **Default `false`.**
76
+ * When on, `many`/`oneOrMore`/`sepBy` in every compiled grammar gain a
77
+ * `_ctx._tolerant`-gated recovery branch (dormant on strict parses — a normal
78
+ * parse is unaffected and allocates nothing). The output stays macro-inlinable
79
+ * (sentinels build via `_ctx._rec`, not `_rp`). Turn it on for grammars you drive
80
+ * from an editor/language service so `run(g, src, { tolerant: true })` recovers on
81
+ * the fast compiled path; leave it off for build-only grammars.
82
+ */
83
+ recovery?: boolean
84
+ /** Emit grammar-coverage hooks in macro output. Off by default, so ordinary
85
+ * macro output remains byte-identical. */
86
+ grammarCoverage?: boolean
87
+ }
88
+
89
+ const PARSEMAN_MODULE = 'parseman'
90
+
91
+ /**
92
+ * The REASON a lowering refused, appended to the warning that reports it.
93
+ *
94
+ * A grammar that fails to lower falls back to the interpreter with the parseman
95
+ * import surviving — it still parses, so no test notices, and the only symptom is
96
+ * ~79 ms against ~14 ms on `benchmark.less`. "Couldn't be inlined" alone does not
97
+ * tell an author which construct to change; the compiler knows, so it says.
98
+ */
99
+ function reasonSuffix(runtimeOnly: readonly string[] | undefined): string {
100
+ return runtimeOnly === undefined || runtimeOnly.length === 0
101
+ ? ''
102
+ : ` — ${runtimeOnly.join('; ')}`
103
+ }
104
+
105
+ /** THE reader for a build-time options object — `rules({ … }, f)`, `compose(…, { … })`.
106
+ *
107
+ * Returns the value expression for `name`, or undefined when the object does not set it.
108
+ * Keys are compared through `propName`, the plugin's single property-key reader, so a
109
+ * QUOTED key is the same key and a COMPUTED key is no key at all.
110
+ *
111
+ * This existed three times, inline, each reading only `key.name`. The runtime path reads
112
+ * a real object and cannot tell `{ hostMode: 'cst' }` from `{ 'hostMode': 'cst' }`; the
113
+ * macro could, and dropped the second — shipping an 'ast' artifact for a source that
114
+ * asked for 'cst', with no warning, which `assertHostModeCompatible` then passed because
115
+ * the artifact genuinely WAS 'ast'. Two correct copies would not have been a fix here;
116
+ * there were three copies and all three were wrong the same way. */
117
+ function optionProp(optExpr: AnyNode | undefined, name: string): Expression | undefined {
118
+ if (optExpr?.type !== 'ObjectExpression') return undefined
119
+ const found = ((optExpr.properties as AnyNode[] | undefined) ?? [])
120
+ .find(p => propName(p as never) === name)
121
+ return (found as { value?: Expression } | undefined)?.value
122
+ }
123
+
124
+ function unwrapStaticExpression(expr: Expression): Expression {
125
+ let cur = expr as unknown as { type?: string; expression?: Expression }
126
+ while (cur.type === 'TSAsExpression'
127
+ || cur.type === 'TSSatisfiesExpression'
128
+ || cur.type === 'TSNonNullExpression'
129
+ || cur.type === 'TSTypeAssertion'
130
+ || cur.type === 'TSInstantiationExpression'
131
+ || cur.type === 'ParenthesizedExpression') {
132
+ if (!cur.expression) break
133
+ cur = cur.expression as unknown as typeof cur
134
+ }
135
+ return cur as Expression
136
+ }
137
+
138
+ function isStaticNullishExpression(expr: Expression): boolean {
139
+ const unwrapped = unwrapStaticExpression(expr) as Expression & { name?: string; value?: unknown }
140
+ return (unwrapped.type === 'Identifier' && unwrapped.name === 'undefined')
141
+ || (unwrapped.type === 'Literal' && unwrapped.value === null)
142
+ }
143
+
144
+ // A grammar's carried linkable pieces live ONLY in its COMPILED output (the macro
145
+ // embeds them there), never in its `.ts` source. So resolving an imported grammar
146
+ // to read its pieces must prefer the built `import`/`require` entry — NOT the
147
+ // `source` condition (which would land on un-compiled `.ts` with no pieces).
148
+ let _compiledResolver: ResolverFactory | null = null
149
+ function getCompiledResolver(): ResolverFactory {
150
+ return _compiledResolver ??= new ResolverFactory({
151
+ extensions: ['.js', '.mjs', '.cjs'],
152
+ conditionNames: ['import', 'require', 'default'],
153
+ mainFields: ['module', 'main'],
154
+ })
155
+ }
156
+
157
+ type ResolvedGrammarModule = {
158
+ file: string
159
+ /** A private TypeScript source module, not a published macro artifact. */
160
+ source: boolean
161
+ }
162
+
163
+ const PRIVATE_SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts'])
164
+
165
+ function nearestPackageRoot(file: string): string | null {
166
+ let dir = path.dirname(file)
167
+ for (;;) {
168
+ try {
169
+ if (fs.statSync(path.join(dir, 'package.json')).isFile()) return fs.realpathSync(dir)
170
+ } catch { /* keep walking upward */ }
171
+ const parent = path.dirname(dir)
172
+ if (parent === dir) return null
173
+ dir = parent
174
+ }
175
+ }
176
+
177
+ function isWithinPackageRoot(file: string, root: string): boolean {
178
+ const relative = path.relative(root, file)
179
+ return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
180
+ }
181
+
182
+ /**
183
+ * Resolve only a relative/absolute source-private import. Published package
184
+ * imports deliberately remain compiled-artifact-only: source is allowed here
185
+ * solely so `import { syntax } from './recognition.js'` can consume the
186
+ * colocated `recognition.ts` before a bundler has emitted that JS file.
187
+ */
188
+ function resolvePrivateSourceModule(from: string, specifier: string): string | null {
189
+ if (!specifier.startsWith('.') && !path.isAbsolute(specifier)) return null
190
+ const packageRoot = nearestPackageRoot(from)
191
+ if (!packageRoot) return null
192
+ const base = path.resolve(path.dirname(from), specifier)
193
+ const ext = path.extname(base)
194
+ const candidates = [
195
+ base,
196
+ ...(ext ? ['.ts', '.tsx', '.mts', '.cts'].map(next => `${base.slice(0, -ext.length)}${next}`) : []),
197
+ ...(!ext ? ['.ts', '.tsx', '.mts', '.cts'].map(next => `${base}${next}`) : []),
198
+ ...(!ext ? ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(next => path.join(base, next)) : []),
199
+ ]
200
+ for (const candidate of candidates) {
201
+ try {
202
+ if (!fs.statSync(candidate).isFile()) continue
203
+ // Both paths are real, so a symlink cannot smuggle a source dependency out
204
+ // of the importing package after the lexical `../` check passed.
205
+ const real = fs.realpathSync(candidate)
206
+ if (isWithinPackageRoot(real, packageRoot)) return real
207
+ } catch { /* try the next source spelling */ }
208
+ }
209
+ return null
210
+ }
211
+
212
+ function resolveGrammarModule(from: string, specifier: string): ResolvedGrammarModule | null {
213
+ // An explicit source import means source semantics even when a stale/generated
214
+ // sibling JS artifact exists. Ordinary `.js` imports keep compiled precedence.
215
+ if (PRIVATE_SOURCE_EXTENSIONS.has(path.extname(specifier))) {
216
+ const file = resolvePrivateSourceModule(from, specifier)
217
+ return file ? { file, source: true } : null
218
+ }
219
+ try {
220
+ const file = getCompiledResolver().resolveFileSync(from, specifier).path
221
+ if (file) return { file, source: false }
222
+ } catch { /* private source fallback below */ }
223
+ const file = resolvePrivateSourceModule(from, specifier)
224
+ return file ? { file, source: true } : null
225
+ }
226
+
227
+ // Recursive source lowering is a build-time operation. This guard makes an
228
+ // import cycle fail closed rather than recursing into a runtime composition.
229
+ const _sourceLowering = new Set<string>()
230
+ const _sourceLoweringCache = new Map<string, { mtimeMs: number; parsed: ParsedModule | null }>()
231
+
232
+ function sourceLoweringCacheKey(
233
+ file: string,
234
+ moduleAliases: Set<string>,
235
+ warnUnloweredRegex: boolean,
236
+ recovery: boolean,
237
+ ): string {
238
+ return `${file}\0${[...moduleAliases].sort().join('\0')}\0${warnUnloweredRegex ? 'warn' : ''}\0${recovery ? 'recovery' : ''}`
239
+ }
240
+
241
+ // Module-level parse cache for imported COMPILED grammar modules (read to recover
242
+ // a grammar's carried pieces for compose()), keyed by absolute file path. Shared
243
+ // across transformMacro invocations so a grammar imported by many consumer files
244
+ // is read + parsed once per build. The mtime guard invalidates a stale entry so a
245
+ // watch-mode rebuild picks up a rebuilt module. A null `parsed` (unreadable / parse
246
+ // error) is cached too, to avoid retrying a known-bad file for every consumer.
247
+ type ParsedModule = { body: unknown[]; src: string }
248
+ const _moduleParseCache = new Map<string, { mtimeMs: number; parsed: ParsedModule | null }>()
249
+
250
+ function parseModuleCached(filePath: string): ParsedModule | null {
251
+ let mtimeMs: number
252
+ try {
253
+ mtimeMs = fs.statSync(filePath).mtimeMs
254
+ } catch {
255
+ return null // file vanished — don't cache under a stale key
256
+ }
257
+ const hit = _moduleParseCache.get(filePath)
258
+ if (hit && hit.mtimeMs === mtimeMs) return hit.parsed
259
+ let parsed: ParsedModule | null = null
260
+ try {
261
+ const src = fs.readFileSync(filePath, 'utf8')
262
+ const r = parseSync(filePath, src)
263
+ if (r.errors.length === 0) parsed = { body: r.program.body as unknown[], src }
264
+ } catch { /* leave null */ }
265
+ _moduleParseCache.set(filePath, { mtimeMs, parsed })
266
+ return parsed
267
+ }
268
+
269
+ export default createUnplugin((opts: ParsecraftPluginOptions = {}) => ({
270
+ name: 'parseman',
271
+ // Run BEFORE the bundler's TS/JS transform — otherwise esbuild (Vite) strips
272
+ // the `with { type: 'macro' }` import attribute before we ever see it, and the
273
+ // macro silently never fires.
274
+ enforce: 'pre' as const,
275
+
276
+ transformInclude(id: string) {
277
+ return /\.[jt]sx?$/.test(id) && !id.includes('node_modules')
278
+ },
279
+
280
+ transform(this: { warn?: (msg: string) => void }, code: string, id: string) {
281
+ if (!code.includes('parseman')) return null
282
+ if (!code.includes('macro')) return null
283
+ const moduleAliases = new Set([PARSEMAN_MODULE, ...(opts.moduleAliases ?? [])])
284
+ const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true)
285
+ if (result?.warnings.length) {
286
+ for (const w of result.warnings) {
287
+ if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
288
+ else console.warn(`[parseman] ${w}`)
289
+ }
290
+ }
291
+ if (!result) return null
292
+ return { code: result.code, map: result.map }
293
+ },
294
+ }))
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Reading carried pieces back out of a COMPILED grammar module
298
+ // ---------------------------------------------------------------------------
299
+ // The macro embeds a grammar's linkable pieces inside its exported const's
300
+ // initializer (see `withCarriedPieces`). To compose an imported grammar we parse
301
+ // its compiled module, find that const, and pull the pieces literal back out —
302
+ // these three pure helpers do exactly that (no runtime, no source recompile).
303
+
304
+ type AnyNode = { type: string; start: number; end: number } & Record<string, unknown>
305
+
306
+ /** Map an export NAME → the local binding it refers to. Handles both
307
+ * `export const X = …` (local === exported) and a bundler's rename
308
+ * `const X$1 = …; export { X$1 as X }`. */
309
+ function exportLocalName(body: AnyNode[], exported: string): string | null {
310
+ for (const st of body) {
311
+ if (st.type !== 'ExportNamedDeclaration') continue
312
+ const decl = st.declaration as AnyNode | undefined
313
+ if (decl?.type === 'VariableDeclaration') {
314
+ for (const d of (decl.declarations as AnyNode[] | undefined) ?? []) {
315
+ const idn = d.id as { type?: string; name?: string } | undefined
316
+ if (idn?.type === 'Identifier' && idn.name === exported) return exported
317
+ }
318
+ }
319
+ for (const sp of (st.specifiers as AnyNode[] | undefined) ?? []) {
320
+ const exp = (sp.exported as { name?: string } | undefined)?.name
321
+ const loc = (sp.local as { name?: string } | undefined)?.name
322
+ if (exp === exported && loc) return loc
323
+ }
324
+ }
325
+ return null
326
+ }
327
+
328
+ /** Find the top-level `const <name> = <init>` initializer node. */
329
+ function topLevelInit(body: AnyNode[], name: string): AnyNode | null {
330
+ for (const st of body) {
331
+ const vd = st.type === 'VariableDeclaration' ? st
332
+ : st.type === 'ExportNamedDeclaration' && (st.declaration as AnyNode | undefined)?.type === 'VariableDeclaration'
333
+ ? (st.declaration as AnyNode) : null
334
+ if (!vd) continue
335
+ for (const d of (vd.declarations as AnyNode[] | undefined) ?? []) {
336
+ const idn = d.id as { type?: string; name?: string } | undefined
337
+ if (idn?.type === 'Identifier' && idn.name === name && d.init) return d.init as AnyNode
338
+ }
339
+ }
340
+ return null
341
+ }
342
+
343
+ /** `Symbol.for('parseman.composedPieces')` ? */
344
+ function isComposedPiecesSymbol(n: AnyNode | undefined): boolean {
345
+ if (!n || n.type !== 'CallExpression') return false
346
+ const callee = n.callee as AnyNode | undefined
347
+ const obj = (callee?.object as { name?: string } | undefined)?.name
348
+ const prop = (callee?.property as { name?: string } | undefined)?.name
349
+ const arg0 = (n.arguments as AnyNode[] | undefined)?.[0] as { value?: unknown } | undefined
350
+ return obj === 'Symbol' && prop === 'for' && arg0?.value === 'parseman.composedPieces'
351
+ }
352
+
353
+ /** Walk an initializer subtree for the `Object.defineProperty(_,
354
+ * Symbol.for('parseman.composedPieces'), { value: <LITERAL> })` the macro emits,
355
+ * and return the source range of <LITERAL>. */
356
+ function findCarriedPiecesLiteral(root: AnyNode): { start: number; end: number } | null {
357
+ let found: { start: number; end: number } | null = null
358
+ const visit = (n: unknown): void => {
359
+ if (found || !n || typeof n !== 'object') return
360
+ if (Array.isArray(n)) { for (const c of n) visit(c); return }
361
+ const node = n as AnyNode
362
+ if (node.type === 'CallExpression') {
363
+ const callee = node.callee as AnyNode | undefined
364
+ const obj = (callee?.object as { name?: string } | undefined)?.name
365
+ const prop = (callee?.property as { name?: string } | undefined)?.name
366
+ const args = node.arguments as AnyNode[] | undefined
367
+ // Table lowering: `tableRules(program, { [Symbol.for('…composedPieces')]:
368
+ // <literal>, … })`. The metadata object becomes the returned map's
369
+ // prototype, so the symbol remains readable without becoming an own key.
370
+ if ((callee as { type?: string; name?: string } | undefined)?.type === 'Identifier'
371
+ && (callee as { name?: string }).name === 'tableRules'
372
+ && (args?.[1] as AnyNode | undefined)?.type === 'ObjectExpression') {
373
+ for (const p of ((args![1] as AnyNode).properties as AnyNode[] | undefined) ?? []) {
374
+ if ((p as { computed?: boolean }).computed && isComposedPiecesSymbol(p.key as AnyNode) && p.value) {
375
+ found = { start: (p.value as AnyNode).start, end: (p.value as AnyNode).end }
376
+ return
377
+ }
378
+ }
379
+ }
380
+ // Also accepted: `Object.assign(_g, { [Symbol.for('…composedPieces')]: <literal> })`
381
+ // (transitional; the current emitter uses the non-enumerable defineProperty form below).
382
+ if (obj === 'Object' && prop === 'assign' && args && (args[1] as AnyNode | undefined)?.type === 'ObjectExpression') {
383
+ for (const p of ((args[1] as AnyNode).properties as AnyNode[] | undefined) ?? []) {
384
+ if ((p as { computed?: boolean }).computed && isComposedPiecesSymbol(p.key as AnyNode) && p.value) {
385
+ found = { start: (p.value as AnyNode).start, end: (p.value as AnyNode).end }
386
+ return
387
+ }
388
+ }
389
+ }
390
+ // Current form: `Object.defineProperty(_g, sym, { value: …, enumerable: false })`.
391
+ if (obj === 'Object' && prop === 'defineProperty' && args && isComposedPiecesSymbol(args[1])) {
392
+ const descriptor = args[2] as AnyNode | undefined
393
+ for (const p of (descriptor?.properties as AnyNode[] | undefined) ?? []) {
394
+ const key = p.key as { name?: string; value?: string } | undefined
395
+ if ((key?.name === 'value' || key?.value === 'value') && p.value) {
396
+ found = { start: (p.value as AnyNode).start, end: (p.value as AnyNode).end }
397
+ return
398
+ }
399
+ }
400
+ }
401
+ }
402
+ for (const k in node) {
403
+ if (k === 'type' || k === 'start' || k === 'end') continue
404
+ const v = (node as Record<string, unknown>)[k]
405
+ if (v && typeof v === 'object') visit(v)
406
+ }
407
+ }
408
+ visit(root)
409
+ return found
410
+ }
411
+
412
+ /** Local-binding → { source, imported } for every `import { … }` in a parsed
413
+ * module body. Used to resolve an ancestor-pieces spread (`...(binding[Symbol…])`)
414
+ * in a compiled grammar back to the module that binding was imported from. */
415
+ function extractImportBindings(body: AnyNode[]): Map<string, { source: string; imported: string }> {
416
+ const out = new Map<string, { source: string; imported: string }>()
417
+ for (const stmt of body) {
418
+ if (stmt.type !== 'ImportDeclaration') continue
419
+ const s = stmt as unknown as ImportDeclaration
420
+ for (const spec of s.specifiers) {
421
+ if (spec.type === 'ImportSpecifier') {
422
+ const imported = (spec.imported as { name?: string }).name ?? spec.local.name
423
+ out.set(spec.local.name, { source: s.source.value, imported })
424
+ }
425
+ }
426
+ }
427
+ return out
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Core transform (exported for testing)
432
+ // ---------------------------------------------------------------------------
433
+
434
+ type ImportInfo = {
435
+ start: number
436
+ end: number
437
+ names: Set<string>
438
+ fullyResolved: boolean // mutated after evaluation
439
+ }
440
+
441
+ export type TransformMacroResult = {
442
+ code: string
443
+ map: ReturnType<MagicString['generateMap']>
444
+ /** Diagnostics for macro-referencing shapes that fell back to the interpreter. */
445
+ warnings: string[]
446
+ }
447
+
448
+ /**
449
+ * Read a source-private grammar as if Vite had already run this macro on it.
450
+ * The result is still only an in-memory artifact for the importing transform:
451
+ * no source module is executed, emitted, or made available at runtime.
452
+ */
453
+ function lowerPrivateSourceModule(
454
+ file: string,
455
+ moduleAliases: Set<string>,
456
+ warnUnloweredRegex: boolean,
457
+ recovery: boolean,
458
+ ): ParsedModule | null {
459
+ if (_sourceLowering.has(file)) return null
460
+ let mtimeMs: number
461
+ try {
462
+ mtimeMs = fs.statSync(file).mtimeMs
463
+ } catch {
464
+ return null
465
+ }
466
+ const cacheKey = sourceLoweringCacheKey(file, moduleAliases, warnUnloweredRegex, recovery)
467
+ const hit = _sourceLoweringCache.get(cacheKey)
468
+ if (hit && hit.mtimeMs === mtimeMs) return hit.parsed
469
+ _sourceLowering.add(file)
470
+ let parsedModule: ParsedModule | null = null
471
+ try {
472
+ const source = fs.readFileSync(file, 'utf8')
473
+ const transformed = transformMacro(source, file, moduleAliases, warnUnloweredRegex, recovery)
474
+ if (!transformed || transformed.warnings.length > 0) return null
475
+ const parsed = parseSync(file, transformed.code)
476
+ parsedModule = parsed.errors.length === 0
477
+ ? { body: parsed.program.body as unknown[], src: transformed.code }
478
+ : null
479
+ return parsedModule
480
+ } catch {
481
+ return null
482
+ } finally {
483
+ _sourceLoweringCache.set(cacheKey, { mtimeMs, parsed: parsedModule })
484
+ _sourceLowering.delete(file)
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Transform one module, ALWAYS releasing the process-global capture state.
490
+ *
491
+ * `beginDegradationCapture()`, `beginLoweringCapture()` and `setReducerResolver()` set
492
+ * module-level globals that the body's straight-line path released at the end. The body
493
+ * also THROWS — `composeLeaf() must macro-fuse` at the `compileComposeLeafCall` site is
494
+ * one of several — and every one of those throws jumped over the release.
495
+ *
496
+ * The consequence was process-wide and silent: one failed macro transform left the
497
+ * degradation sink OPEN forever, so every later `recordDegradation` — including from an
498
+ * unrelated runtime `compile()` in the same process — was filed into an orphaned Map that
499
+ * nobody would ever drain, and printed nothing. Measured: 0 `console.warn` calls after an
500
+ * aborted capture, 1 finding stranded in the orphan.
501
+ *
502
+ * So the release lives in a `finally`, and whatever the failed frame had collected is
503
+ * printed rather than dropped — a degradation that was real before the abort is still
504
+ * real after it.
505
+ */
506
+ export function transformMacro(
507
+ code: string,
508
+ id: string,
509
+ moduleAliases = new Set([PARSEMAN_MODULE]),
510
+ warnUnloweredRegex = false,
511
+ recovery = false,
512
+ grammarCoverage = false,
513
+ ): TransformMacroResult | null {
514
+ const depth = degradationCaptureDepth()
515
+ try {
516
+ return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage)
517
+ } finally {
518
+ setReducerResolver(null)
519
+ // Both are idempotent: on the success path the body already released them and these
520
+ // are no-ops. On an aborted transform they are what stops the leak.
521
+ for (const d of unwindDegradationCapture(depth)) console.warn(formatDegradation(d))
522
+ }
523
+ }
524
+
525
+ function transformMacroImpl(
526
+ code: string,
527
+ id: string,
528
+ moduleAliases: Set<string>,
529
+ warnUnloweredRegex: boolean,
530
+ recovery: boolean,
531
+ grammarCoverage: boolean,
532
+ ): TransformMacroResult | null {
533
+ let result: ReturnType<typeof parseSync>
534
+ try {
535
+ result = parseSync(id, code)
536
+ } catch {
537
+ return null
538
+ }
539
+ if (result.errors.length > 0) return null
540
+
541
+ const body = result.program.body
542
+
543
+ // --- Pass 1: collect macro imports + non-macro import bindings ---
544
+ const macroImports: ImportInfo[] = []
545
+ const allNames = new Set<string>()
546
+ // local name -> { source module, imported name }, for every NON-macro named
547
+ // import. Lets a `...frag(g)` spread whose factory is imported (tier 2) be
548
+ // resolved from the exporting module's source at build time.
549
+ const importBindings = new Map<string, { source: string; imported: string }>()
550
+ const ordinaryImports: Array<{
551
+ start: number
552
+ end: number
553
+ specifiers: Array<{ type: string; local: string; start: number; end: number }>
554
+ }> = []
555
+
556
+ for (const stmt of body) {
557
+ if (stmt.type !== 'ImportDeclaration') continue
558
+ const s = stmt as ImportDeclaration
559
+
560
+ // A macro import is a parseman-alias import carrying `with { type: 'macro' }`
561
+ // (oxc exposes this as ImportDeclaration.attributes).
562
+ const isMacro = moduleAliases.has(s.source.value) && s.attributes.some(a => {
563
+ const key = a.key.type === 'Identifier' ? a.key.name : String(a.key.value)
564
+ return key === 'type' && a.value.value === 'macro'
565
+ })
566
+
567
+ if (!isMacro) {
568
+ const specifiers: Array<{ type: string; local: string; start: number; end: number }> = []
569
+ for (const spec of s.specifiers) {
570
+ if (spec.type === 'ImportSpecifier') {
571
+ const imported = (spec.imported as { name?: string }).name ?? spec.local.name
572
+ importBindings.set(spec.local.name, { source: s.source.value, imported })
573
+ specifiers.push({ type: spec.type, local: spec.local.name, start: spec.start, end: spec.end })
574
+ } else {
575
+ specifiers.push({ type: spec.type, local: spec.local.name, start: spec.start, end: spec.end })
576
+ }
577
+ }
578
+ ordinaryImports.push({ start: s.start, end: s.end, specifiers })
579
+ continue
580
+ }
581
+
582
+ const names = new Set<string>()
583
+ for (const spec of s.specifiers) {
584
+ if (spec.type === 'ImportSpecifier') names.add(spec.local.name)
585
+ }
586
+ macroImports.push({ start: s.start, end: s.end, names, fullyResolved: false })
587
+ for (const n of names) allNames.add(n)
588
+ }
589
+
590
+ if (macroImports.length === 0) return null
591
+
592
+ const unwrapVd = (stmt: Statement): VariableDeclaration | null =>
593
+ stmt.type === 'VariableDeclaration'
594
+ ? (stmt as unknown as VariableDeclaration)
595
+ : stmt.type === 'ExportNamedDeclaration'
596
+ && (stmt as unknown as ExportNamedDeclaration).declaration?.type === 'VariableDeclaration'
597
+ ? ((stmt as unknown as ExportNamedDeclaration).declaration as unknown as VariableDeclaration)
598
+ : null
599
+
600
+ /*
601
+ * Module-level `const <name> = (g) => …` factory declarations, so a `rules()` call
602
+ * can be handed its factory BY NAME instead of inline.
603
+ *
604
+ * This is what makes one grammar source compilable twice:
605
+ *
606
+ * const factory = (g) => ({ … })
607
+ * export const grammar = rules({ trivia: rw }, factory)
608
+ * export const cstGrammar = rules({ trivia: rw, hostMode: 'cst' }, factory)
609
+ *
610
+ * Without it, `rules({ hostMode: 'cst' }, factory)` reports "factory isn't statically
611
+ * evaluable" and falls back to the interpreter, and the ONE thing compile-time host
612
+ * mode exists to enable — two artifacts from one source — cannot be written down. The
613
+ * only alternative is duplicating the whole factory at both call sites.
614
+ *
615
+ * Deliberately narrow: a top-level `const` function-valued binding, matched by name.
616
+ * Anything else still takes the existing inline path.
617
+ *
618
+ * The narrowness is load-bearing, because this substitutes a stored initializer for a
619
+ * NAME and so has to reproduce the binding semantics the name actually has:
620
+ *
621
+ * - `const` ONLY. A `let`/`var` factory can be reassigned between the declaration and
622
+ * the `rules()` call, and substituting the initializer would compile a grammar the
623
+ * program does not have. `const` makes reassignment a syntax error, so matching only
624
+ * `const` removes the case rather than trying to detect it.
625
+ * - DECLARED BEFORE USE. The map is built from the whole module body up front, so
626
+ * without a position check a `rules(…, factory)` sitting ABOVE `const factory = …`
627
+ * would resolve here while the real program throws a temporal-dead-zone
628
+ * ReferenceError. Suppressing an error the source would raise is the same class of
629
+ * defect as emitting the wrong grammar. `declaredAt` is compared against the call
630
+ * site below.
631
+ */
632
+ const factoryDecls = new Map<string, { fn: Expression; declaredAt: number; code?: string; scope?: Scope; imported?: boolean }>()
633
+ for (const stmt of body) {
634
+ const decl = stmt.type === 'ExportNamedDeclaration'
635
+ ? (stmt as unknown as { declaration?: unknown }).declaration
636
+ : stmt
637
+ if ((decl as { type?: string } | undefined)?.type !== 'VariableDeclaration') continue
638
+ // `let`/`var` are rebindable; only `const` can be substituted by name safely.
639
+ if ((decl as unknown as { kind?: string }).kind !== 'const') continue
640
+ for (const d of (decl as unknown as VariableDeclaration).declarations) {
641
+ const id = d.id as unknown as { type: string; name?: string }
642
+ const init = d.init as Expression | null | undefined
643
+ if (id.type !== 'Identifier' || !id.name || !init) continue
644
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
645
+ factoryDecls.set(id.name, {
646
+ fn: init,
647
+ declaredAt: (d as unknown as { end: number }).end,
648
+ })
649
+ }
650
+ }
651
+ }
652
+
653
+ /*
654
+ * Reducer resolution, for `node(..., build)` where `build` is a NAME rather than an
655
+ * inline function.
656
+ *
657
+ * `buildSrc` is the call site's EXPRESSION text, so a named reducer arrives as just the
658
+ * name — no parameter list — and the capture-tier analysis used to fail open and charge
659
+ * the node all five facilities. Sharing reducers across a grammar and importing them
660
+ * from another module is ordinary grammar authoring, so the resolver does the analysis
661
+ * (lexical scope tree, cross-module import following) rather than reporting that it
662
+ * won't. See `reducer-resolver.ts` for what it decides and what genuinely declines.
663
+ *
664
+ * Everything it produces — `_def.buildArity`, `_def.buildSigSrc` — is ANALYSIS-ONLY and
665
+ * never emitted, so the generated builder reference is unchanged either way.
666
+ */
667
+ const reducerResolver = createReducerResolver(id, body as unknown[], code)
668
+ setReducerResolver(reducerResolver, code)
669
+
670
+ const topLevelFunction = (moduleBody: AnyNode[], name: string): AnyNode | null => {
671
+ for (const st of moduleBody) {
672
+ const decl = st.type === 'ExportNamedDeclaration'
673
+ ? (st as unknown as { declaration?: AnyNode }).declaration
674
+ : st
675
+ const idn = decl?.type === 'FunctionDeclaration'
676
+ ? (decl as unknown as { id?: { name?: string } }).id
677
+ : undefined
678
+ if (idn?.name === name) return decl ?? null
679
+ }
680
+ return null
681
+ }
682
+ const isParsemanMacroImport = (stmt: AnyNode): boolean => {
683
+ if (stmt.type !== 'ImportDeclaration') return false
684
+ const s = stmt as unknown as ImportDeclaration
685
+ return moduleAliases.has(s.source.value) && s.attributes.some(a => {
686
+ const key = a.key.type === 'Identifier' ? a.key.name : String(a.key.value)
687
+ return key === 'type' && a.value.value === 'macro'
688
+ })
689
+ }
690
+ const sourceScopeUntil = (moduleBody: AnyNode[], source: string, until: number): Scope | null => {
691
+ const out: Scope = new Map()
692
+ for (const stmt of moduleBody as Statement[]) {
693
+ if (stmt.type === 'ImportDeclaration') {
694
+ if (!isParsemanMacroImport(stmt as unknown as AnyNode)) return null
695
+ continue
696
+ }
697
+ if (stmt.type === 'FunctionDeclaration') continue
698
+ if (stmt.type === 'ExportNamedDeclaration' && (stmt as unknown as ExportNamedDeclaration).declaration?.type === 'FunctionDeclaration') continue
699
+ if (stmt.type === 'ExportNamedDeclaration' && !(stmt as unknown as ExportNamedDeclaration).declaration) {
700
+ if ((stmt as unknown as { source?: unknown }).source) return null
701
+ continue
702
+ }
703
+ const vd = unwrapVd(stmt)
704
+ if (!vd || (vd as unknown as { kind?: string }).kind !== 'const') return null
705
+ for (const decl of vd.declarations) {
706
+ const d = decl as VariableDeclarator
707
+ const idn = d.id as unknown as { type?: string; name?: string }
708
+ const init = d.init as Expression | null | undefined
709
+ if ((idn.type !== 'Identifier' && idn.type !== 'BindingIdentifier') || !idn.name || !init) return null
710
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') continue
711
+ const mapFnSources: string[] = []
712
+ const combi = evaluateExpr(init, out, source, mapFnSources)
713
+ if (combi) {
714
+ if (stmt.start < until) out.set(idn.name, { combi, mfSrcs: mapFnSources })
715
+ continue
716
+ }
717
+ const wordFactory = evaluateWordFactory(init, out, source)
718
+ if (wordFactory) {
719
+ if (stmt.start < until) {
720
+ ;(out as Map<string, unknown>).set(idn.name, wordFactory)
721
+ }
722
+ continue
723
+ }
724
+ const whenFactory = evaluateWhenFactory(init, out, source)
725
+ if (whenFactory) {
726
+ if (stmt.start < until) {
727
+ ;(out as Map<string, unknown>).set(idn.name, whenFactory)
728
+ }
729
+ continue
730
+ }
731
+ const combiArray = evaluateCombinatorArray(init, out, source)
732
+ if (combiArray) {
733
+ if (stmt.start < until) {
734
+ ;(out as Map<string, unknown>).set(idn.name, combiArray)
735
+ }
736
+ continue
737
+ }
738
+ return null
739
+ }
740
+ continue
741
+ }
742
+ return out
743
+ }
744
+ const usedImportedFactories = new Set<string>()
745
+ const markUsedImportedFactories = (names: readonly string[] | undefined): void => {
746
+ for (const name of names ?? []) usedImportedFactories.add(name)
747
+ }
748
+ for (const [localName, binding] of importBindings) {
749
+ const file = resolvePrivateSourceModule(id, binding.source)
750
+ if (!file) continue
751
+ const mod = parseModuleCached(file)
752
+ if (!mod) continue
753
+ const localFor = exportLocalName(mod.body as AnyNode[], binding.imported)
754
+ const init = localFor ? topLevelInit(mod.body as AnyNode[], localFor) ?? topLevelFunction(mod.body as AnyNode[], localFor) : null
755
+ if (!init || (init.type !== 'ArrowFunctionExpression' && init.type !== 'FunctionExpression' && init.type !== 'FunctionDeclaration')) continue
756
+ const factoryScope = sourceScopeUntil(mod.body as AnyNode[], mod.src, init.start)
757
+ if (!factoryScope) continue
758
+ factoryDecls.set(localName, {
759
+ fn: init as unknown as Expression,
760
+ declaredAt: -1,
761
+ code: mod.src,
762
+ scope: factoryScope,
763
+ imported: true,
764
+ })
765
+ // This factory is evaluated with `code: mod.src`, so every `node(…, build)` inside
766
+ // it hands the resolver an offset into THIS file. Tell the resolver the file exists
767
+ // so it answers against that module's own scope tree instead of declining
768
+ // `foreign-source` — a decline is sound but charges the node full capture.
769
+ reducerResolver.register(file)
770
+ }
771
+
772
+ // --- Pass 2: evaluate declarations in source order ---
773
+ // Scope stores enriched ScopeEntry objects so evaluateParserFactory can
774
+ // replay mfSrcs when outer-scope combinators are referenced inside factories.
775
+ const scope: Scope = new Map<string, ScopeEntry>()
776
+ const replacements: Array<{ start: number; end: number; replacement: string }> = []
777
+ const warnings: string[] = []
778
+ // Collect degradations instead of printing them, so they arrive on the SAME channel
779
+ // as every other macro warning (the bundler's `this.warn`) with a `file:line` anchor.
780
+ beginDegradationCapture()
781
+ let anyUnresolved = false
782
+ // A declaration whose emitted value still CALLS a macro import, without anything
783
+ // having failed: a shared shape keeps its `rules(…)` source (the interpreter map is
784
+ // the only correct standalone value for a grammar with a hole) while still carrying
785
+ // fully compiled pieces for downstream composition. The import must survive, but
786
+ // this is not an unresolved shape, so it neither warns nor blocks other cleanups.
787
+ let keepMacroImport = false
788
+ /**
789
+ * Set when any emitted replacement is a TABLE, which references the shared
790
+ * driver `tableRules` by name.
791
+ *
792
+ * This is the one CONTRACT DIVERGENCE the table lowering states openly
793
+ * (`table/compile.ts`): a table expression is not self-contained, because the
794
+ * driver being external is exactly why the artifact is 0.56 MB rather than
795
+ * 2.10 MB. Inlining the driver per grammar rebuilds the size this lowering
796
+ * exists to remove. So the plugin owns the import, and the free-identifier net
797
+ * at the end of this transform is what proves it was actually emitted.
798
+ */
799
+ let usedTableRuntime = false
800
+ /** Exported `rules()` factories, whose bodies are left verbatim. See the push site. */
801
+ const exportedFactories: Array<{ name: string; pos: number }> = []
802
+ let runtimeComposeFallback = false
803
+ // Unique per rules() call site in this file — holds the ONE shared compiled
804
+ // rule-map object; each destructured local name reads its property off it.
805
+ let ruleMapHolderCounter = 0
806
+
807
+ // Surface a shape the macro couldn't compile (it silently runs via the
808
+ // interpreter otherwise). Includes a file:line anchor so it's actionable.
809
+ const lineOf = (pos: number): number => {
810
+ let line = 1
811
+ for (let i = 0; i < pos && i < code.length; i++) if (code.charCodeAt(i) === 10) line++
812
+ return line
813
+ }
814
+ const warn = (pos: number, msg: string): void => {
815
+ anyUnresolved = true
816
+ warnings.push(`${id}:${lineOf(pos)} — ${msg} (running via the interpreter; add the plugin or simplify the declaration to compile it)`)
817
+ }
818
+
819
+ /**
820
+ * Compile a `rules(factory)` call into ONE shared replacement expression
821
+ * for the whole call — see compileRuleMap() in codegen.ts for why this is
822
+ * one shared codegen pass instead of one `compile()` per entry (a `rules()`
823
+ * factory's entries commonly share large reachable sub-rule graphs; a
824
+ * shared pass compiles each shared sub-rule once instead of once per entry
825
+ * that reaches it).
826
+ */
827
+ const evaluateRulesFactory = (
828
+ init: Expression,
829
+ label: string,
830
+ ): { ruleMap: Map<string, Combinator<unknown>>; trivia?: Combinator<unknown>; scanSkip?: Combinator<unknown>[]; trackLines?: boolean; importedFactory?: string } | null => {
831
+ const args = (init as unknown as { arguments: unknown[] }).arguments
832
+ // Options-first: rules({ trivia }, factory). Disambiguate by type — an
833
+ // ObjectExpression first arg means options lead; otherwise the factory leads
834
+ // (bare rules(factory), or the tolerated legacy rules(factory, { trivia })).
835
+ const arg0 = args[0] as AnyNode | undefined
836
+ const arg1 = args[1] as AnyNode | undefined
837
+ const optionsFirst = arg0?.type === 'ObjectExpression'
838
+ const factoryArgRaw = (optionsFirst ? arg1 : arg0) as Expression | undefined
839
+ if (!factoryArgRaw) { warn(init.start, `${label}: rules() needs a factory argument`); return null }
840
+ /* A factory named by identifier resolves to its module-level declaration, so two
841
+ * `rules()` call sites can SHARE one factory — see `factoryDecls`. Only when the
842
+ * declaration PRECEDES this call: a later one is a temporal dead zone in the real
843
+ * program, and resolving it here would compile a grammar out of a binding that does
844
+ * not exist yet. Falling through leaves `factoryArgRaw`, which takes the ordinary
845
+ * inline path and reports "isn't statically evaluable" rather than inventing an answer. */
846
+ const namedFactory = factoryArgRaw.type === 'Identifier'
847
+ ? factoryDecls.get((factoryArgRaw as unknown as { name: string }).name)
848
+ : undefined
849
+ const factoryArg = namedFactory !== undefined && namedFactory.declaredAt <= init.start
850
+ ? namedFactory.fn
851
+ : factoryArgRaw
852
+ const factoryScope = namedFactory?.scope ?? scope
853
+ const factoryCode = namedFactory?.code ?? code
854
+ const importedFactory = namedFactory?.imported === true && factoryArg === namedFactory.fn
855
+ ? (factoryArgRaw as unknown as { name?: string }).name
856
+ : undefined
857
+
858
+ // Grammar-level options object — evaluate `trivia` / `scanSkip` so the compiled
859
+ // map seeds them as the ambient defaults (build-time mirror of rules() tagging
860
+ // grammarTrivia / grammarScanSkip at runtime).
861
+ const optionsArg = (optionsFirst ? arg0 : arg1) as AnyNode | undefined
862
+ const optionValue = (name: string): Expression | undefined => optionProp(optionsArg, name)
863
+
864
+ // Read and VALIDATE hostMode before evaluating the factory, so a mode the macro
865
+ // cannot honour is reported even when the factory also fails to evaluate. Getting
866
+ // this order wrong means the one option whose silent loss is a wrong TREE gets
867
+ // swallowed by an unrelated warning.
868
+ const hostModeValue = optionValue('hostMode')
869
+ const gHostMode = hostModeValue?.type === 'Literal'
870
+ ? (hostModeValue as unknown as { value?: unknown }).value
871
+ : undefined
872
+ if (hostModeValue !== undefined && gHostMode !== 'ast' && gHostMode !== 'cst') {
873
+ warn(init.start, `${label}: rules({ hostMode }) must be the literal 'ast' or 'cst'`)
874
+ return null
875
+ }
876
+ const trackLinesValue = optionValue('trackLines')
877
+ const gTrackLines = trackLinesValue?.type === 'Literal'
878
+ ? (trackLinesValue as unknown as { value?: unknown }).value
879
+ : undefined
880
+ if (trackLinesValue !== undefined && typeof gTrackLines !== 'boolean') {
881
+ warn(init.start, `${label}: rules({ trackLines }) must be a boolean literal`)
882
+ return null
883
+ }
884
+
885
+ // A specific reason when the factory BODY is what failed. The generic message
886
+ // points nowhere near the cause, and the dominant real cause — a forward
887
+ // reference to a const declared lower down — is one the interpreter reports
888
+ // precisely. Two lanes lost a round to the generic text.
889
+ /*
890
+ * THE OPTIONS GO IN, THEY ARE NOT REAPPLIED AFTERWARDS.
891
+ *
892
+ * Three loops used to live below this point, stamping `grammarScanSkip`,
893
+ * `grammarHostMode` and `grammarTrackLines` onto the evaluated rules, each
894
+ * carrying a comment that the macro "evaluates the FACTORY directly and never
895
+ * calls `rules()`". It does now, so the options are threaded as the
896
+ * `RulesOptions` argument and `rules()` applies them — the same code, on the
897
+ * same rules, at the same point, for the runtime and the build.
898
+ *
899
+ * That also fixes the `trackLines` half, which the stamp-only copy got wrong:
900
+ * `rules()` does not merely mark the rules, it WRAPS each non-trivia rule in a
901
+ * `grammarParser({ trackLines: true })` scope (`parser.ts:228-242`). The macro
902
+ * carried the setting to the encoder by a different route instead, so the two
903
+ * routes built structurally different maps for the same source.
904
+ *
905
+ * Evaluated BEFORE the factory because they are now inputs to it.
906
+ */
907
+ const triviaValue = optionValue('trivia')
908
+ const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : undefined
909
+ const scanSkipValue = optionValue('scanSkip')
910
+ const gScanSkip = scanSkipValue
911
+ ? (evaluateCombinatorArray(scanSkipValue, scope, code) ?? undefined)
912
+ : undefined
913
+
914
+ const why: { reason?: string } = {}
915
+ const ruleMap = evaluateParserFactory(factoryArg, factoryScope, factoryCode, [], why, {
916
+ ...(gTrivia ? { trivia: gTrivia as Combinator<unknown> } : {}),
917
+ ...(gScanSkip ? { scanSkip: gScanSkip } : {}),
918
+ ...(gHostMode === 'cst' ? { hostMode: 'cst' as const } : {}),
919
+ ...(gTrackLines === true ? { trackLines: true } : {}),
920
+ })
921
+ if (!ruleMap) {
922
+ warn(init.start, why.reason === undefined
923
+ ? `${label}: rules(...) factory isn't statically evaluable`
924
+ : `${label}: rules(...) factory isn't statically evaluable — ${why.reason}`)
925
+ return null
926
+ }
927
+
928
+ return {
929
+ ruleMap,
930
+ ...(gTrivia ? { trivia: gTrivia } : {}),
931
+ ...(gScanSkip ? { scanSkip: gScanSkip } : {}),
932
+ ...(gTrackLines === true ? { trackLines: true } : {}),
933
+ ...(importedFactory ? { importedFactory } : {}),
934
+ }
935
+ }
936
+
937
+ /** null → the factory itself isn't statically evaluable (already warned).
938
+ * `replacement: null` → the map evaluated fine but `compileRuleMap` couldn't
939
+ * inline it; the caller decides whether that is a fallback-to-interpreter
940
+ * warning or the SHARED-SHAPE case (an unresolved external `g.` ref, which is
941
+ * un-inlinable by construction but still linkable — see `hasExternalRef`). */
942
+ const compileRulesFactory = (
943
+ init: Expression,
944
+ label: string,
945
+ ): { replacement: string | null; replacementWithMetadata?: (metadataSource: string) => string; refusals?: readonly string[]; ruleMap: Map<string, Combinator<unknown>>; hostMode: HostMode; hostBranchElided: boolean; reflection: GrammarReflection; coverageDefinitions?: readonly { id: string; kind: string }[]; trivia?: Combinator<unknown>; scanSkip?: Combinator<unknown>[]; trackLines?: boolean; importedFactory?: string } | null => {
946
+ const evaluated = evaluateRulesFactory(init, label)
947
+ if (!evaluated) return null
948
+ // The reasons, out of the encoder and into the warning. A bare "couldn't be
949
+ // inlined" leaves the author with a ~5x silent perf regression and no lead.
950
+ const refusals: string[] = []
951
+ const compiled = compileRuleMap([...evaluated.ruleMap], { ...(evaluated.trivia ? { trivia: evaluated.trivia } : {}), ...(evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}), ...(evaluated.trackLines ? { trackLines: true } : {}), recovery, coverage: grammarCoverage, refusals })
952
+ // A table replacement names `tableRules`, which nothing in the consumer's
953
+ // module binds. That reference is the whole reason the artifact is small (the
954
+ // driver is SHARED, not inlined per grammar), so the import is owned here.
955
+ if (compiled !== null) usedTableRuntime = true
956
+ return {
957
+ replacement: compiled?.replacement ?? null,
958
+ ...(compiled === null ? {} : { replacementWithMetadata: compiled.replacementWithMetadata }),
959
+ ...(compiled === null ? { refusals } : {}),
960
+ ruleMap: evaluated.ruleMap,
961
+ hostMode: compiled?.hostMode ?? 'ast',
962
+ hostBranchElided: compiled?.hostBranchElided ?? false,
963
+ reflection: compiled?.reflection ?? { nodes: [] },
964
+ ...(compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}),
965
+ ...(evaluated.trivia ? { trivia: evaluated.trivia } : {}),
966
+ ...(evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}),
967
+ ...(evaluated.trackLines ? { trackLines: true } : {}),
968
+ ...(evaluated.importedFactory ? { importedFactory: evaluated.importedFactory } : {}),
969
+ }
970
+ }
971
+
972
+ const isRulesCall = (init: Expression): boolean =>
973
+ init.type === 'CallExpression' &&
974
+ (init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier' &&
975
+ (init as unknown as { callee: { name?: string } }).callee.name === 'rules'
976
+
977
+ const isComposeCall = (init: Expression): boolean =>
978
+ init.type === 'CallExpression' &&
979
+ (init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier' &&
980
+ (init as unknown as { callee: { name?: string } }).callee.name === 'compose'
981
+
982
+ const isComposeLeafCall = (init: Expression): boolean =>
983
+ init.type === 'CallExpression'
984
+ && (init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier'
985
+ && (init as unknown as { callee: { name?: string } }).callee.name === 'composeLeaf'
986
+
987
+ // Local `rules()` grammars, so a same-file `compose([myRules, …])` can recover
988
+ // the pieces to fuse (name → the rule map evaluated at build). A grammar stays a
989
+ // usable parser AND is composable — no opt-in wrapper.
990
+ const localRuleMaps = new Map<string, Map<string, Combinator<unknown>>>()
991
+ // Local plain grammars (`const g = rules({ trivia }, …)`) that declared a
992
+ // grammar-level trivia — read by composingTrivia() so a same-file
993
+ // `compose([…, g])` can adopt g's trivia (composing-wins). A grammar declared
994
+ // WITHOUT trivia is absent here, so composingTrivia keeps scanning earlier items.
995
+ const localGrammarTrivia = new Map<string, Combinator<unknown>>()
996
+ // …and its grammar-level scanSkip, so a same-file `composeLeaf([g, …])` where the
997
+ // final local grammar is an IDENTIFIER bound to `rules({ scanSkip })` still threads
998
+ // that opaque-unit set to the fused local piece (mirrors the inline-rules() case).
999
+ const localGrammarScanSkip = new Map<string, Combinator<unknown>[]>()
1000
+
1001
+ // Stable, reproducible per-artifact namespace: hash of module id + a label
1002
+ // (binding name / arg position) — never a counter, so rebuilds are byte-stable
1003
+ // and two artifacts never collide when fused.
1004
+ const nsFor = (label: string): string =>
1005
+ `_${createHash('sha1').update(`${id}#${label}`).digest('hex').slice(0, 8)}_`
1006
+
1007
+ /** Serialize a pieces LIST — one entry for a `rules()` grammar, the flattened
1008
+ * list for a `compose()` result. */
1009
+ // A carried list entry is either a grammar's own pieces (serialized in full) or
1010
+ // an IMPORT MARKER standing in for an imported ancestor grammar's pieces. The
1011
+ // marker keeps a descendant from re-serializing everything it imported (Less
1012
+ // re-shipping CSS, SCSS re-shipping css+less, …): at read time the marker is
1013
+ // resolved back to the ancestor's compiled file transitively. Build-time only —
1014
+ // importedPieces resolves it via the module resolver; runtime never reads it.
1015
+ // An imported ancestor is referenced by SPREADING its live carried pieces off the
1016
+ // imported binding — `...(cssGrammar[Symbol.for('parseman.composedPieces')] ?? [])`.
1017
+ // At runtime the ES import is a live value, so this self-resolves (transitively:
1018
+ // the ancestor's own value already spread ITS ancestors). At macro-compile time
1019
+ // the plugin resolves the spread statically by following the import. Either way
1020
+ // the ancestor isn't re-serialized. `local` is the binding name as it appears in
1021
+ // THIS module's source (the bundler renames import + references together).
1022
+ type ImportSpread = { __spreadLocal: string }
1023
+ type IRItem = { ns: string; ir: string; trackLines?: true }
1024
+ type CarriedItem = ImportSpread | IRItem
1025
+ const isSpread = (it: CarriedItem): it is ImportSpread => '__spreadLocal' in it
1026
+ const isIR = (it: CarriedItem): it is IRItem => 'ir' in it
1027
+ // A carried entry is: an import SPREAD (live ref to an ancestor's pieces) or an IR
1028
+ // PIECE (this grammar's own rules as a compact combinator expression, re-lowered at
1029
+ // fuse). There is no third form — a piece that cannot serialize is not carried.
1030
+ const serializeItem = (it: CarriedItem): string =>
1031
+ isSpread(it)
1032
+ ? `...(${it.__spreadLocal}[Symbol.for('parseman.composedPieces')] ?? [])`
1033
+ : `{ ns: ${JSON.stringify(it.ns)}, ir: ${JSON.stringify(it.ir)}${it.trackLines ? ', trackLines: true' : ''} }`
1034
+ const serializeList = (list: CarriedItem[]): string => `[${list.map(serializeItem).join(', ')}]`
1035
+
1036
+ type StaticTableMetadata = {
1037
+ carried?: CarriedItem[]
1038
+ reflection?: GrammarReflection
1039
+ leaf?: true
1040
+ }
1041
+ /** Metadata passed into `tableRules` at construction. `stampRuleMap` uses this
1042
+ * object as the returned map's prototype, so these ordinary symbol fields are
1043
+ * readable but never copied by object spread/Object.assign. Coverage comes
1044
+ * directly from the emitted program's `cv` pool and host mode from `h`. */
1045
+ const staticTableMetadataSource = (metadata: StaticTableMetadata): string => {
1046
+ const fields = [
1047
+ ...(metadata.carried === undefined ? [] : [
1048
+ `[Symbol.for('parseman.composedPieces')]: ${serializeList(metadata.carried)}`,
1049
+ ]),
1050
+ ...(metadata.reflection === undefined ? [] : [
1051
+ `[Symbol.for('parseman.grammarReflection')]: ${grammarReflectionSource(metadata.reflection)}`,
1052
+ ]),
1053
+ ...(metadata.leaf === true ? [`[Symbol.for('parseman.leafComposed')]: true`] : []),
1054
+ ]
1055
+ return `{ ${fields.join(', ')} }`
1056
+ }
1057
+
1058
+ /**
1059
+ * Attach a compiled grammar's linkable pieces onto the value, under
1060
+ * `Symbol.for('parseman.composedPieces')` — the same symbol `compose()` reads at
1061
+ * runtime. This is why `import { cssGrammar }` is all a downstream package needs:
1062
+ * the pieces travel WITH the grammar (no detached, tree-shakeable `__pieces`
1063
+ * export). `Object.defineProperty` returns the grammar, so it stays one
1064
+ * expression that can't be shaken off the exported const without dropping the
1065
+ * grammar; `importedPieces()` reads it back out. The descriptor mirrors the
1066
+ * runtime attach (`linker.ts`) — `enumerable: false` so the (large) carried IR
1067
+ * isn't dragged along by `Object.assign(target, grammar)` or `{ ...grammar }`.
1068
+ */
1069
+ const withCarriedPieces = (grammarExpr: string, list: CarriedItem[]): string =>
1070
+ `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.composedPieces'), { value: ${serializeList(list)}, enumerable: false })`
1071
+ /** Coverage-only macro output carries the exact IDs emitted in its generated
1072
+ * hooks. The metadata is non-enumerable so grammar maps keep their ordinary
1073
+ * public shape, and it is absent entirely from production builds. */
1074
+ const withCoverageDefinitions = (grammarExpr: string, definitions: readonly { id: string; kind: string }[]): string =>
1075
+ !grammarCoverage ? grammarExpr
1076
+ : `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.grammarCoverageDefinitions'), { value: Object.freeze(${JSON.stringify(definitions)}.map(Object.freeze)), enumerable: false })`
1077
+ /**
1078
+ * Recover the coverage DENOMINATOR by scraping the IDs out of the generated hooks.
1079
+ *
1080
+ * This is a fallback: it reads emitted source with a regex, so it silently returns `[]`
1081
+ * if the emitted hook shape ever changes or the hooks are absent. An empty denominator
1082
+ * is NOT zero coverage — it is NO MEASUREMENT — and it used to travel all the way to a
1083
+ * consumer's gate as 100%. `'coverage-definitions-unavailable'` is the declared code for
1084
+ * exactly this and had no record site anywhere; this is it.
1085
+ */
1086
+ const emittedCoverageDefinitions = (source: string, where: string): Array<{ id: string; kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' }> => {
1087
+ const ids = new Set<string>()
1088
+ for (const match of source.matchAll(/id:\s*"([^"]+)"/g)) ids.add(match[1]!)
1089
+ if (grammarCoverage && ids.size === 0) {
1090
+ recordDegradation({
1091
+ code: 'coverage-definitions-unavailable',
1092
+ severity: 'warn',
1093
+ where,
1094
+ subject: 'generated coverage hooks',
1095
+ fellBackTo: 'no coverage definitions could be read out of the generated source, so the '
1096
+ + 'grammar carries an EMPTY definition set — which is no measurement, not full coverage',
1097
+ otherwise: 'the emitted hook IDs would form the coverage denominator',
1098
+ })
1099
+ }
1100
+ return [...ids].sort().map(id => ({
1101
+ id,
1102
+ kind: id.startsWith('rule:') ? 'rule' : id.startsWith('label:') ? 'label' : id.startsWith('dispatch:') ? 'dispatch-arm' : 'choice-arm',
1103
+ }))
1104
+ }
1105
+ // Same-file `const X = compose([...])` → its carried (re-lowerable) list, so a
1106
+ // later same-file compose can chain it AND re-lower it under that compose's own
1107
+ // composing trivia (composing-wins holds at every level).
1108
+ const localComposedCarried = new Map<string, CarriedItem[]>()
1109
+
1110
+ // Cache of RE-LOWERABLE pieces lists read from imported COMPILED grammars'
1111
+ // carried pieces (IR pieces + spreads left un-materialized, so the composing
1112
+ // grammar's trivia can be applied when they are finally lowered).
1113
+ type RawItem = IRItem
1114
+ const importedPiecesCache = new Map<string, RawItem[] | null>()
1115
+
1116
+ /**
1117
+ * Read an imported grammar's pieces LIST straight off its COMPILED value —
1118
+ * the `Symbol.for('parseman.composedPieces')` literal the macro embeds inside
1119
+ * the exported const's initializer (see `withCarriedPieces`). No `__pieces`
1120
+ * export, no TS source, no recompile. `import { <name> }` carries everything.
1121
+ * null if the grammar wasn't macro-compiled (or isn't source-free composable).
1122
+ */
1123
+ // Resolve the FULLY-EXPANDED pieces list from a compiled grammar's carried
1124
+ // literal. The literal may SPREAD imported ancestors' pieces —
1125
+ // `...(binding[Symbol.for('parseman.composedPieces')] ?? [])` — which the runtime
1126
+ // resolves off the live import. Here (build time, no live module) we resolve each
1127
+ // spread by following that module's own import of `binding` to its compiled file
1128
+ // and recursing, then eval the literal with the resolved ancestors stubbed in so
1129
+ // the spreads splice their pieces. `seen` guards against an import cycle.
1130
+ const resolveModulePieces = (module: ResolvedGrammarModule, exportName: string, seen: Set<string>): RawItem[] | null => {
1131
+ const { file } = module
1132
+ const mod = module.source
1133
+ ? lowerPrivateSourceModule(file, moduleAliases, warnUnloweredRegex, recovery)
1134
+ : parseModuleCached(file)
1135
+ if (!mod) return null
1136
+ const localFor = exportLocalName(mod.body as AnyNode[], exportName)
1137
+ const initNode = localFor ? topLevelInit(mod.body as AnyNode[], localFor) : null
1138
+ const literalRange = initNode ? findCarriedPiecesLiteral(initNode) : null
1139
+ if (!literalRange) return null
1140
+ const literal = mod.src.slice(literalRange.start, literalRange.end)
1141
+
1142
+ const imports = extractImportBindings(mod.body as AnyNode[])
1143
+ const stubNames: string[] = []
1144
+ const stubVals: unknown[] = []
1145
+ const spreadRe = /\.\.\.\s*\(?\s*([A-Za-z_$][\w$]*)\s*\[\s*Symbol\s*\.\s*for\s*\(\s*['"]parseman\.composedPieces['"]/g
1146
+ const done = new Set<string>()
1147
+ for (let m: RegExpExecArray | null; (m = spreadRe.exec(literal)); ) {
1148
+ const local = m[1]!
1149
+ if (done.has(local)) continue
1150
+ done.add(local)
1151
+ const b = imports.get(local)
1152
+ let subPieces: RawItem[] = []
1153
+ if (b) {
1154
+ const subModule = resolveGrammarModule(file, b.source)
1155
+ if (subModule && !seen.has(subModule.file)) {
1156
+ subPieces = resolveModulePieces(subModule, b.imported, new Set(seen).add(subModule.file)) ?? []
1157
+ }
1158
+ }
1159
+ stubNames.push(local)
1160
+ // The KEY comes from the linker, which owns it. Spelling `Symbol.for('…')`
1161
+ // here made the string the contract between a build-time module and a
1162
+ // runtime one, with nothing checking: renaming it in `linker.ts` would have
1163
+ // left this stub keying on the old name, and a carried-pieces lookup that
1164
+ // silently finds nothing produces a grammar that composes to less than it
1165
+ // should — no type error, no failing test. (The `Symbol.for('…')` spellings
1166
+ // further down are EMITTED SOURCE TEXT, not this module's own key: they are
1167
+ // strings by necessity and the runtime resolves them through the registry.)
1168
+ stubVals.push({ [COMPOSED_PIECES]: subPieces })
1169
+ }
1170
+ try {
1171
+ // Build-time eval of the carried literal with ancestor spreads stubbed — NOT
1172
+ // runtime. `Symbol.for`/`Map` are globals; `stubNames` provide the imports.
1173
+ // The result stays RE-LOWERABLE: IR pieces ({ns, ir}) are NOT materialized here,
1174
+ // so the composing grammar's trivia can be seeded when they are finally lowered
1175
+ // (materializeCarried). Ancestor spreads were already resolved (also un-lowered).
1176
+ // eslint-disable-next-line no-new-func
1177
+ const list = new Function(...stubNames, `return (${literal})`)(...stubVals) as RawItem[]
1178
+ return list
1179
+ } catch { return null }
1180
+ }
1181
+
1182
+ const importedPieces = (localName: string): RawItem[] | null => {
1183
+ if (importedPiecesCache.has(localName)) return importedPiecesCache.get(localName)!
1184
+ let result: RawItem[] | null = null
1185
+ const binding = importBindings.get(localName)
1186
+ if (binding) {
1187
+ const module = resolveGrammarModule(id, binding.source)
1188
+ result = module ? resolveModulePieces(module, binding.imported, new Set([module.file])) : null
1189
+ }
1190
+ importedPiecesCache.set(localName, result)
1191
+ return result
1192
+ }
1193
+
1194
+ /**
1195
+ * MERGE a carried list to ONE rule map — the table lowering's entire composition
1196
+ * mechanism, and the reason it needs no linker.
1197
+ *
1198
+ * Source composition is a TEXTUAL splice: each piece is lowered on its own to
1199
+ * namespaced `_r_<Name>` functions, and `fusedBody()` picks a winner per name and
1200
+ * substitutes the `@FS:` dispatch placeholders with that winner's first-set
1201
+ * condition. A table has no text to splice and no placeholders to resolve, so the
1202
+ * merge moves one level UP, onto the combinators: evaluate each piece's IR back to
1203
+ * a rule map, let a later piece's name override an earlier one — which is exactly
1204
+ * what `compose()` means — and hand the single merged map to
1205
+ * `compileRuleMap`, which encodes ONCE.
1206
+ *
1207
+ * This is what makes table composition the easy kind: no relocation of code
1208
+ * offsets, no merging of const / fn / class / expected / dispatch pools between
1209
+ * two already-encoded programs. `encodeTableProgram` points `enc.winners` at the
1210
+ * merged map (`table/encode.ts:1383`), so a base piece's internal `g.Atom`
1211
+ * resolves BY NAME to the override (`:1036`) rather than through a thunk that
1212
+ * closes over the base. Open recursion — much of the point of compose — therefore
1213
+ * survives the merge, and the result is the table the merged grammar would have
1214
+ * produced had it been written as a single `rules()` call.
1215
+ *
1216
+ * `null` means some piece cannot contribute combinators: a FULL BAKED piece (the
1217
+ * un-serializable fallback at `localCarried`) carries lowered SOURCE and nothing
1218
+ * else. That shape is codegen-only, so the caller falls back to fused source
1219
+ * rather than refusing the grammar.
1220
+ */
1221
+ type CarriedRuleMap = { ns: string; rules: Array<[string, Combinator<unknown>]> }
1222
+ const carriedRuleMaps = (
1223
+ items: CarriedItem[],
1224
+ ): { pieces: CarriedRuleMap[]; trackLines: boolean } | null => {
1225
+ const pieces: CarriedRuleMap[] = []
1226
+ let trackLines = false
1227
+ const add = (item: RawItem): boolean => {
1228
+ if (!isIR(item)) return false
1229
+ // `trackLines` rides on the CARRIED ITEM, not inside the IR (`IRPiece.trackLines`
1230
+ // is a sibling field of `ir`), so it has to be lifted here. Left behind, the
1231
+ // merged encode resolves `trackLines` from `_meta` alone and silently drops line
1232
+ // tracking for a grammar that asked for it.
1233
+ if (item.trackLines === true) trackLines = true
1234
+ pieces.push({ ns: item.ns, rules: evalRuleMapIR(item.ir) })
1235
+ return true
1236
+ }
1237
+ for (const item of items) {
1238
+ if (isSpread(item)) {
1239
+ const imported = importedPieces(item.__spreadLocal)
1240
+ if (!imported || !imported.every(add)) return null
1241
+ } else if (!add(item as RawItem)) return null
1242
+ }
1243
+ return { pieces, trackLines }
1244
+ }
1245
+
1246
+ /**
1247
+ * Fold ordered rule maps into the composed map: a later piece's name WINS, and
1248
+ * `Map` keeps each name at its first-sighting position so the encoded rule order
1249
+ * tracks the source lowering's rather than drifting per compose.
1250
+ *
1251
+ * A REFERENCE IS NOT A DEFINITION. A `rules(g => …)` cache also holds every `g.X`
1252
+ * that was merely ACCESSED, so a delta that calls `g.Pair` carries a `Pair` entry
1253
+ * that is an unresolved named lazy. Merged in name order that entry lands LAST and
1254
+ * shadows the base piece which actually defines `Pair` — the encoder then finds a
1255
+ * hole where the winner should be and refuses the whole grammar with "ref() used
1256
+ * before .define()". The same filter guards `compileLinkableTable`'s `isLocal`
1257
+ * (`compile-linkable-table.ts:96`), `itemCarried` (`linker.ts:696`) and
1258
+ * `recoverComposedRules`; this is the fourth site that needs it, for the same
1259
+ * reason, and skipping it is why cross-piece composition looked blocked.
1260
+ *
1261
+ * The reference itself is not lost: it stays inside the delta's own rule bodies and
1262
+ * `enc.winners` binds it BY NAME to whichever piece supplies the definition.
1263
+ */
1264
+ const mergeRuleMaps = (
1265
+ maps: ReadonlyArray<ReadonlyArray<readonly [string, Combinator<unknown>]>>,
1266
+ ): Array<[string, Combinator<unknown>]> => {
1267
+ const winners = new Map<string, Combinator<unknown>>()
1268
+ for (const map of maps) {
1269
+ for (const [name, rule] of map) {
1270
+ if (rule._def.tag === 'lazy') {
1271
+ try { rule._def.thunk() } catch { continue }
1272
+ }
1273
+ winners.set(name, rule)
1274
+ }
1275
+ }
1276
+ return [...winners]
1277
+ }
1278
+
1279
+ const mergedCarriedRules = (
1280
+ items: CarriedItem[],
1281
+ ): { rules: Array<[string, Combinator<unknown>]>; trackLines: boolean } | null => {
1282
+ const carriedMaps = carriedRuleMaps(items)
1283
+ if (carriedMaps === null) return null
1284
+ return { rules: mergeRuleMaps(carriedMaps.pieces.map(p => p.rules)), trackLines: carriedMaps.trackLines }
1285
+ }
1286
+
1287
+ /**
1288
+ * COMPOSING-WINS, as an OVERRIDE rather than a gap-fill.
1289
+ *
1290
+ * `compileRuleMap`'s `applyAmbient` only fills a rule that carries no trivia of
1291
+ * its own — correct for `composeLeaf`, whose pieces may legitimately disagree, and
1292
+ * WRONG for `compose`, where the composing grammar's trivia governs every fused rule
1293
+ * INCLUDING the inherited ones. Gap-filling leaves a base rule that declared its own
1294
+ * `rules({ trivia }, …)` still skipping the base's trivia after a delta re-declared
1295
+ * it, so `compose([css, less])` silently parses the inherited rules under css's
1296
+ * whitespace — the multi-level composing-wins contract, inverted.
1297
+ *
1298
+ * Safe to mutate: every rule here came from `evalRuleMapIR`, which constructs FRESH
1299
+ * combinators per piece per compose, so this cannot leak into another compose of the
1300
+ * same base grammar.
1301
+ */
1302
+ const applyComposingTrivia = (
1303
+ ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
1304
+ composing: Combinator<unknown>,
1305
+ ): void => {
1306
+ for (const [, rule] of ruleMap) {
1307
+ if (rule._meta.isTrivia) continue
1308
+ ;(rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = composing
1309
+ }
1310
+ }
1311
+
1312
+ /** Resolve one `compose([...])` argument to its RE-LOWERABLE carried items (IR
1313
+ * pieces / import spreads / baked-pieces fallback). The composing grammar's trivia
1314
+ * is applied later, uniformly, in materializeCarried — never per-piece here. */
1315
+ // A local rule map → its compact IR ({ns, ir}) for carrying; the trivia is NOT baked
1316
+ // into the IR (it is seeded at re-lower time). When the map can't be faithfully
1317
+ // serialized, fall back to full lowered pieces WITH the composing trivia baked in.
1318
+ const localCarried = (rm: Iterable<readonly [string, unknown]>, label: string, composing?: Combinator<unknown>, scanSkip?: Combinator<unknown>[]): { carried: CarriedItem[] } | null => {
1319
+ const entries = [...rm]
1320
+ const ns = nsFor(label)
1321
+ const trackLines = entries.some(([, rule]) => (rule as Combinator<unknown> | undefined)?._meta?.grammarTrackLines === true)
1322
+ // scanSkip is PER-PIECE (opaque units are dialect-specific — NOT composing-wins
1323
+ // like trivia), so it rides WITH this element's IR: it is emitted into the carried
1324
+ // `rules({ scanSkip }, …)` options, which stamps `_meta.grammarScanSkip` when the IR
1325
+ // is re-lowered (materializePiece → compileLinkable picks it up), and survives to a
1326
+ // downstream re-compose. The full-pieces fallback bakes it via the compile option.
1327
+ const ir = serializeRuleMap(entries as never, scanSkip)
1328
+ if (ir) return { carried: [{ ns, ir, ...(trackLines ? { trackLines: true as const } : {}) }] }
1329
+ // NO FULL-PIECES FALLBACK. It used to bake the grammar to lowered source when the IR
1330
+ // would not serialize; with one lowering there is nothing to bake to, and IR is not
1331
+ // an optimization here but the representation composition is built on.
1332
+ //
1333
+ // This is not a capability loss. The branch had no test fixture and not for want of
1334
+ // trying: every callback-source trigger in `serializeRuleMap` is pre-empted by the
1335
+ // macro's own stricter guard (a direct builder must be "macro-static and
1336
+ // self-contained", which throws first), and every "unsupported tag" trigger is a
1337
+ // ChoiceStrategy tag (`types.ts:405-408`), not a ParserDef tag, so it never reaches
1338
+ // that switch. It fired ZERO times across jess's whole five-package compose chain.
1339
+ // Refusing here leaves the runtime `compose()` in place, which is correct.
1340
+ warn(0, `${label}: rule map could not be serialized to IR, so it cannot be carried for composition. `
1341
+ + 'Re-run with PARSEMAN_IR_DEBUG=1 to print the exact combinator that blocked serialization.')
1342
+ return null
1343
+ }
1344
+
1345
+ const argPieces = (arg: Expression, label: string, composing?: Combinator<unknown>): { carried: CarriedItem[]; importedFactories?: string[] } | null => {
1346
+ // Inline `rules(g => …)` or `rules({ trivia }, g => …)` (options-first). The
1347
+ // element's OWN trivia option is ignored for lowering — composing-wins means the
1348
+ // composing grammar's trivia (computed in compileComposeCall) governs every
1349
+ // fused rule, this element's included. It only matters as a CANDIDATE for the
1350
+ // composing trivia itself, which composingTrivia() reads directly off the AST.
1351
+ if (isRulesCall(arg)) {
1352
+ // This element's OWN scanSkip DOES thread (per-piece, unlike composing-wins
1353
+ // trivia): evaluateRulesFactory returns both the rule map and the grammar-level
1354
+ // scanSkip option, which localCarried carries with this piece's IR.
1355
+ const evaluated = evaluateRulesFactory(arg, label)
1356
+ const carried = evaluated ? localCarried(evaluated.ruleMap, label, composing, evaluated.scanSkip) : null
1357
+ return carried
1358
+ ? { ...carried, ...(evaluated?.importedFactory ? { importedFactories: [evaluated.importedFactory] } : {}) }
1359
+ : null
1360
+ }
1361
+ if (arg.type === 'Identifier') {
1362
+ const name = (arg as unknown as { name: string }).name
1363
+ // Local grammar var (`const myRules = rules(...)`).
1364
+ const rm = localRuleMaps.get(name)
1365
+ if (rm) return localCarried(rm, label, composing, localGrammarScanSkip.get(name))
1366
+ // Local composed var (`const g = compose([...])`) → its own carried list, which
1367
+ // materializeCarried re-lowers under THIS compose's composing trivia.
1368
+ const composed = localComposedCarried.get(name)
1369
+ if (composed) return { carried: composed }
1370
+ // Imported grammar — carry a live spread of the imported binding (so we don't
1371
+ // re-serialize an ancestor); resolved + re-lowered in materializeCarried.
1372
+ const pieces = importedPieces(name)
1373
+ if (!pieces) return null
1374
+ return { carried: importBindings.has(name) ? [{ __spreadLocal: name }] : [...pieces] }
1375
+ }
1376
+ return null
1377
+ }
1378
+
1379
+ /** The composing (outermost) grammar's trivia: the LAST array element that is a
1380
+ * plain grammar declaring `trivia` — an inline `rules({ trivia }, …)` (options-first
1381
+ * OR legacy trailing-options) or a local `const g = rules({ trivia }, …)`. Composed
1382
+ * sub-results and imported compiled grammars do NOT contribute (they carry no
1383
+ * re-surfaced trivia identity) — mirrors runtime composingTriviaOf skipping
1384
+ * COMPOSED_PIECES / already-compiled artifacts. */
1385
+ const rulesCallTrivia = (el: Expression): Combinator<unknown> | undefined => {
1386
+ const rulesArgs = (el as unknown as { arguments: unknown[] }).arguments
1387
+ const a0 = rulesArgs[0] as AnyNode | undefined
1388
+ const a1 = rulesArgs[1] as AnyNode | undefined
1389
+ const optExpr = (a0?.type === 'ObjectExpression' ? a0 : a1?.type === 'ObjectExpression' ? a1 : undefined) as AnyNode | undefined
1390
+ const triviaValue = optionProp(optExpr, 'trivia')
1391
+ if (!triviaValue) return undefined
1392
+ return (evaluateExpr(triviaValue, scope, code, []) as Combinator<unknown> | null) ?? undefined
1393
+ }
1394
+ const composingTrivia = (elements: ReadonlyArray<Expression | null>): Combinator<unknown> | undefined => {
1395
+ for (let i = elements.length - 1; i >= 0; i--) {
1396
+ const el = elements[i]
1397
+ if (!el) continue
1398
+ if (isRulesCall(el)) {
1399
+ const t = rulesCallTrivia(el)
1400
+ if (t) return t
1401
+ continue
1402
+ }
1403
+ if (el.type === 'Identifier') {
1404
+ const t = localGrammarTrivia.get((el as unknown as { name: string }).name)
1405
+ if (t) return t
1406
+ // local composed / imported compiled grammar → contributes rules, not trivia.
1407
+ }
1408
+ }
1409
+ return undefined
1410
+ }
1411
+
1412
+ /** Compile `compose([...])` to STATIC fused source (eval-free) + its carried
1413
+ * (re-lowerable) list (for a sidecar / same-file chaining). null → leave the
1414
+ * runtime `compose()` in place (correct, just not build-fused). */
1415
+ const compileComposeCall = (init: Expression): { replacement: string; exportedReplacement: string; carried: CarriedItem[]; trivia?: Combinator<unknown>; importedFactories?: string[]; coverageDefinitions?: readonly { id: string; kind: string }[] } | null => {
1416
+ const args = (init as unknown as { arguments: Expression[] }).arguments
1417
+ const arr = args[0]
1418
+ if (!arr || arr.type !== 'ArrayExpression') {
1419
+ warn(init.start, 'compose(): expected a static array of grammars/artifacts')
1420
+ return null
1421
+ }
1422
+ const elements = (arr as unknown as { elements: Expression[] }).elements
1423
+ // `compose(items, { hostMode })` — read and VALIDATE it, mirroring `rules()`. This
1424
+ // argument used to be ignored entirely: `args[1]` was never read, so a macro build of
1425
+ // `compose(items, { hostMode: 'cst' })` silently produced an 'ast' artifact while the
1426
+ // runtime path honoured the option. Silently is the operative word — the caller asked
1427
+ // for a CST artifact, got an eval-AST one, and the compatibility assertion then passed
1428
+ // because the artifact genuinely WAS 'ast'. Same vacuous-classification shape this
1429
+ // change exists to remove, one call site over.
1430
+ const cOptions = (init as unknown as { arguments: Expression[] }).arguments[1] as AnyNode | undefined
1431
+ const cHostModeValue = optionProp(cOptions, 'hostMode')
1432
+ const cHostMode = cHostModeValue?.type === 'Literal'
1433
+ ? (cHostModeValue as unknown as { value?: unknown }).value
1434
+ : undefined
1435
+ if (cHostModeValue !== undefined && cHostMode !== 'ast' && cHostMode !== 'cst') {
1436
+ warn(init.start, "compose({ hostMode }) must be the literal 'ast' or 'cst'")
1437
+ return null
1438
+ }
1439
+ // Composing-wins (B): ONE composing trivia, from the last plain grammar in the
1440
+ // list that declares one, governs EVERY fused rule — including inherited ones.
1441
+ const composing = composingTrivia(elements)
1442
+ const carried: CarriedItem[] = [] // re-lowerable; also SERIALIZED onto the value
1443
+ const importedFactories: string[] = []
1444
+ for (let i = 0; i < elements.length; i++) {
1445
+ const r = argPieces(elements[i]!, `compose${init.start}_${i}`, composing)
1446
+ if (!r) { warn(init.start, `compose(): argument ${i} isn't a build-resolvable grammar; falling back to runtime`); return null }
1447
+ carried.push(...r.carried)
1448
+ importedFactories.push(...(r.importedFactories ?? []))
1449
+ }
1450
+ // Lower the whole list ONCE, seeding the composing trivia into every re-lowerable
1451
+ // piece (composing-wins), then fuse.
1452
+ // TABLE FIRST. The merged map IS the composed grammar (see `mergedCarriedRules`),
1453
+ // so `compose()` lowers through the SAME `compileRuleMap` a plain `rules()`
1454
+ // does — one encode, one `tableRules(…)` expression, no linker. `carried` is
1455
+ // unchanged either way: it is the re-lowerable IR list, not a lowering artifact,
1456
+ // so a downstream re-compose behaves identically whichever engine emitted here.
1457
+ const merged = mergedCarriedRules(carried)
1458
+ if (merged !== null) {
1459
+ if (composing) applyComposingTrivia(merged.rules, composing)
1460
+ const refusals: string[] = []
1461
+ const compiled = compileRuleMap(merged.rules, {
1462
+ ...(composing ? { trivia: composing } : {}),
1463
+ ...(merged.trackLines ? { trackLines: true } : {}),
1464
+ ...(cHostMode ? { hostMode: cHostMode as HostMode } : {}),
1465
+ recovery,
1466
+ coverage: grammarCoverage,
1467
+ refusals,
1468
+ })
1469
+ if (compiled !== null) {
1470
+ usedTableRuntime = true
1471
+ const reflectionMetadata = staticTableMetadataSource({ reflection: compiled.reflection })
1472
+ const exportedMetadata = staticTableMetadataSource({
1473
+ carried,
1474
+ reflection: compiled.reflection,
1475
+ })
1476
+ return {
1477
+ replacement: compiled.replacementWithMetadata(reflectionMetadata),
1478
+ exportedReplacement: compiled.replacementWithMetadata(exportedMetadata),
1479
+ carried,
1480
+ ...(composing ? { trivia: composing } : {}),
1481
+ ...(importedFactories.length ? { importedFactories } : {}),
1482
+ // The AUTHORITATIVE denominator, from `buildGrammarPlan` via
1483
+ // `compileRuleMapTable`. It was computed here and dropped, leaving the
1484
+ // compose() call site with nothing but the regex scrape — see the call site.
1485
+ ...(compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}),
1486
+ }
1487
+ }
1488
+ // There is no second lowering to fall back TO. Leaving the runtime `compose()`
1489
+ // call in place is correct (the runtime path fuses the same merged map), just not
1490
+ // build-fused — so name the reason rather than reporting a bare refusal.
1491
+ warn(init.start, `compose(): could not be lowered to a table; leaving the runtime compose() in place${reasonSuffix(refusals)}`)
1492
+ return null
1493
+ }
1494
+ warn(init.start, 'compose(): a carried piece has no re-lowerable IR; leaving the runtime compose() in place')
1495
+ return null
1496
+ }
1497
+
1498
+ /**
1499
+ * Compile a terminal composition. Imported/base pieces still travel as normal
1500
+ * re-lowerable IR, but the final local rules map is compiled directly in this
1501
+ * module. Its direct builders may therefore refer to lexical AST constructors;
1502
+ * they are inlined into the fused output and are never serialized or carried.
1503
+ */
1504
+ const compileComposeLeafCall = (init: Expression): { replacement: string; importedFactories?: string[] } | null => {
1505
+ const args = (init as unknown as { arguments: Expression[] }).arguments
1506
+ const arr = args[0]
1507
+ if (!arr || arr.type !== 'ArrayExpression') {
1508
+ warn(init.start, 'composeLeaf(): expected a static array of grammars')
1509
+ return null
1510
+ }
1511
+ const elements = (arr as unknown as { elements: Expression[] }).elements
1512
+ if (elements.length < 2) {
1513
+ warn(init.start, 'composeLeaf(): needs imported recognition grammar(s) and one local rules() map')
1514
+ return null
1515
+ }
1516
+ const localArg = elements[elements.length - 1]!
1517
+ let localRules: Iterable<readonly [string, unknown]> | null = null
1518
+ // The LOCAL leaf grammar's own ambient scanSkip (from its `rules({ scanSkip })`)
1519
+ // governs the scanTo/balanced sites IT defines — unlike trivia (composing-wins),
1520
+ // opaque units are dialect-specific, so the local declaration is threaded to the
1521
+ // local piece's compile.
1522
+ let localScanSkip: Combinator<unknown>[] | undefined
1523
+ const importedFactories: string[] = []
1524
+ if (isRulesCall(localArg)) {
1525
+ const evaluated = evaluateRulesFactory(localArg, `composeLeaf${init.start}`)
1526
+ localRules = evaluated?.ruleMap ?? null
1527
+ localScanSkip = evaluated?.scanSkip
1528
+ if (evaluated?.importedFactory) importedFactories.push(evaluated.importedFactory)
1529
+ } else if (localArg.type === 'Identifier') {
1530
+ const name = (localArg as unknown as { name: string }).name
1531
+ localRules = localRuleMaps.get(name) ?? null
1532
+ localScanSkip = localGrammarScanSkip.get(name)
1533
+ }
1534
+ if (!localRules) {
1535
+ // Distinguish "the argument is the wrong SHAPE" from "the argument is the right
1536
+ // shape and failed to evaluate". Only the first is a composeLeaf problem. When a
1537
+ // `rules()` call is present it has already warned with the specific cause (a
1538
+ // forward reference, a non-static callback); repeating a shape complaint here
1539
+ // buries it and sends the reader looking at the composeLeaf argument list. That
1540
+ // is exactly how this signature got reported twice as a grammar defect.
1541
+ const named = localArg.type === 'Identifier' ? (localArg as unknown as { name: string }).name : null
1542
+ warn(init.start, isRulesCall(localArg)
1543
+ ? 'composeLeaf(): the final rules() map failed to evaluate — see the warning above for the cause'
1544
+ : named !== null
1545
+ ? `composeLeaf(): final argument \`${named}\` did not resolve to a local rules() map.`
1546
+ + ` It must be a \`const ${named} = rules(...)\` in THIS module, declared before this call`
1547
+ + ` — an imported or re-exported grammar cannot be the leaf.`
1548
+ : 'composeLeaf(): final argument must be a local rules() map')
1549
+ return null
1550
+ }
1551
+ const composing = composingTrivia(elements)
1552
+ const carried: CarriedItem[] = []
1553
+ for (let i = 0; i < elements.length - 1; i++) {
1554
+ const r = argPieces(elements[i]!, `composeLeaf${init.start}_${i}`, composing)
1555
+ if (!r) {
1556
+ warn(init.start, `composeLeaf(): argument ${i} isn't a build-resolvable recognition grammar`)
1557
+ return null
1558
+ }
1559
+ carried.push(...r.carried)
1560
+ importedFactories.push(...(r.importedFactories ?? []))
1561
+ }
1562
+ try {
1563
+ // The imported pieces are recognition-only, but the local leaf grammar
1564
+ // may place one beneath a direct node(). Re-lower their terminals with
1565
+ // capture enabled so that node receives the imported token values in its
1566
+ // normal child collector; the pieces still contain no semantic callback.
1567
+ const localNs = nsFor(`composeLeaf${init.start}`)
1568
+ const localEntries = [...localRules] as Array<[string, Combinator<unknown>]>
1569
+
1570
+ // TABLE FIRST — same merge as `compose()`, with the local leaf map appended LAST
1571
+ // so it wins every name and binds the imported shapes' holes (`enc.winners`
1572
+ // resolves those by name; see `mergedCarriedRules`).
1573
+ //
1574
+ // The recognition-only gate is preserved EXACTLY, and is the reason this path
1575
+ // needs no `compileLinkable`: `hasDirectBuilders` / `isRecognitionOnly` are
1576
+ // predicates over the combinator graph, not products of lowering it, so
1577
+ // `classifyRuleMap` answers both from the piece's own rule map. Lowering every
1578
+ // piece to read two booleans off `LinkablePieces` was the only thing making this
1579
+ // gate look codegen-shaped.
1580
+ //
1581
+ // Coverage needs no `materializeLeafCoverage` counterpart here. That helper
1582
+ // exists because coverage ids are WeakMap-keyed and planning from a second IR
1583
+ // hydration would leave the EMITTED pieces uninstrumented; the table plans from
1584
+ // the merged map it then encodes, so the planned identities and the encoded
1585
+ // identities are the same objects by construction.
1586
+ const carriedMaps = carriedRuleMaps(carried)
1587
+ if (carriedMaps !== null) {
1588
+ const unproven = carriedMaps.pieces.find(p => {
1589
+ const c = classifyRuleMap(p.rules)
1590
+ return c.hasDirectBuilders || !c.isRecognitionOnly
1591
+ })
1592
+ if (unproven) {
1593
+ warn(init.start, 'composeLeaf(): every pre-final grammar must explicitly prove recognition-only')
1594
+ return null
1595
+ }
1596
+ // The local grammar's OWN ambient scanSkip is stamped onto the LOCAL entries
1597
+ // only. Passing it as a merged-map option instead would let `applyAmbient`
1598
+ // hand it to every imported rule that happens to carry no stamp of its own —
1599
+ // opaque units are dialect-specific, and that is precisely the leak the
1600
+ // per-piece threading in the source path exists to avoid.
1601
+ if (localScanSkip) {
1602
+ for (const [, rule] of localEntries) {
1603
+ if (rule._meta.isTrivia) continue
1604
+ const meta = rule._meta as { grammarScanSkip?: Combinator<unknown>[] }
1605
+ if (meta.grammarScanSkip === undefined) meta.grammarScanSkip = localScanSkip
1606
+ }
1607
+ }
1608
+ const leafMerged = mergeRuleMaps([...carriedMaps.pieces.map(p => p.rules), localEntries])
1609
+ // Composing-wins governs the leaf fuse too. The local entries are NOT freshly
1610
+ // evaluated (they come from this module's own `rules()` factory), so they are
1611
+ // excluded from the override — their declared trivia IS the composing candidate,
1612
+ // and rewriting `_meta` on them would mutate the module-level grammar object.
1613
+ if (composing) applyComposingTrivia(carriedMaps.pieces.flatMap(p => p.rules), composing)
1614
+ const refusals: string[] = []
1615
+ const compiled = compileRuleMap(
1616
+ leafMerged,
1617
+ {
1618
+ ...(composing ? { trivia: composing } : {}),
1619
+ ...(carriedMaps.trackLines ? { trackLines: true } : {}),
1620
+ recovery,
1621
+ coverage: grammarCoverage,
1622
+ refusals,
1623
+ },
1624
+ )
1625
+ if (compiled !== null) {
1626
+ usedTableRuntime = true
1627
+ return {
1628
+ replacement: compiled.replacementWithMetadata(staticTableMetadataSource({
1629
+ reflection: compiled.reflection,
1630
+ leaf: true,
1631
+ })),
1632
+ ...(importedFactories.length ? { importedFactories } : {}),
1633
+ }
1634
+ }
1635
+ warn(init.start, `composeLeaf(): could not be lowered to a table${reasonSuffix(refusals)}`)
1636
+ return null
1637
+ }
1638
+ // `composeLeaf` is TERMINAL and macro-only: there is no runtime composition to
1639
+ // fall back to (the caller turns this null into a hard throw), and now no second
1640
+ // lowering either. A piece with no re-lowerable IR is the end of the road.
1641
+ warn(init.start, 'composeLeaf(): a recognition piece has no re-lowerable IR')
1642
+ return null
1643
+ } catch (e) {
1644
+ warn(init.start, `composeLeaf(): ${(e as Error).message}`)
1645
+ return null
1646
+ }
1647
+ }
1648
+
1649
+ // --- Pre-pass: resolve standalone ref() recursion clusters ---
1650
+ // `const x = ref()` … `x.define(expr)` is the interpreter/compile() recursion
1651
+ // mechanism. The macro must support it for parity: evaluate every ref slot and
1652
+ // apply every `.define(...)` into scope BEFORE the main loop compiles anything,
1653
+ // so codegen's emitLazy sees a defined thunk (otherwise it falls back to the
1654
+ // interpreter). `.define(...)` statements are stripped from the output since
1655
+ // they reference the now-removed import. Only a ref whose define resolves is
1656
+ // treated as a macro ref; an unresolved one falls through to the normal warn().
1657
+ // First detect whether any ref() cluster exists; only then do the (more
1658
+ // involved) full-scope pre-pass, keeping ordinary macro files on the fast path.
1659
+ const refNames = new Set<string>()
1660
+ for (const stmt of body as Statement[]) {
1661
+ const innerVd = unwrapVd(stmt)
1662
+ if (!innerVd) continue
1663
+ for (const decl of innerVd.declarations) {
1664
+ const d = decl as VariableDeclarator
1665
+ if (!d.init || (d.id as unknown as { type: string }).type !== 'Identifier') continue
1666
+ const init = d.init as Expression
1667
+ if (init.type === 'CallExpression'
1668
+ && (init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier'
1669
+ && (init as unknown as { callee: { name?: string } }).callee.name === 'ref') {
1670
+ refNames.add((d.id as unknown as { name: string }).name)
1671
+ }
1672
+ }
1673
+ }
1674
+
1675
+ // For a ref cluster we must fully populate scope (refs AND the regular consts a
1676
+ // `.define(expr)` references) in source order, so each define resolves before
1677
+ // the main loop compiles. `.define(...)` statements are stripped from the
1678
+ // output since they reference the now-removed import. A ref whose define never
1679
+ // resolves falls through to the normal warn() path in the main loop.
1680
+ const defineRemovals: Array<{ start: number; end: number }> = []
1681
+ if (refNames.size > 0) {
1682
+ for (const stmt of body as Statement[]) {
1683
+ if (stmt.type === 'ExpressionStatement') {
1684
+ const expr = (stmt as unknown as { expression: Expression }).expression
1685
+ if (applyDefineStatement(expr, scope, code)) {
1686
+ defineRemovals.push({ start: stmt.start, end: stmt.end })
1687
+ }
1688
+ continue
1689
+ }
1690
+ const innerVd = unwrapVd(stmt)
1691
+ if (!innerVd) continue
1692
+ for (const decl of innerVd.declarations) {
1693
+ const d = decl as VariableDeclarator
1694
+ if (!d.init || (d.id as unknown as { type: string }).type !== 'Identifier') continue
1695
+ const name = (d.id as unknown as { name: string }).name
1696
+ const init = d.init as Expression
1697
+ if (evaluateRefDeclaration(init, name, scope)) continue
1698
+ if (scope.has(name)) continue
1699
+ if (!referencesAny(init, allNames, scope)) continue
1700
+ // Evaluate into scope so a subsequent `.define()` can reference it. The
1701
+ // main loop re-evaluates and compiles; refs stay shared, so this is safe.
1702
+ const combi = evaluateExpr(init, scope, code, [])
1703
+ if (combi) scope.set(name, { combi, mfSrcs: [] })
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ for (const stmt of body as Statement[]) {
1709
+ // Handle both direct VariableDeclarations and exported ones
1710
+ let vd: VariableDeclaration | null = null
1711
+ let stmtStart = stmt.start
1712
+ let stmtEnd = stmt.end
1713
+ let exportPrefix = ''
1714
+
1715
+ if (stmt.type === 'VariableDeclaration') {
1716
+ vd = stmt as unknown as VariableDeclaration
1717
+ } else if (stmt.type === 'ExportNamedDeclaration') {
1718
+ const expStmt = stmt as unknown as ExportNamedDeclaration
1719
+ if (expStmt.declaration?.type === 'VariableDeclaration') {
1720
+ vd = expStmt.declaration as unknown as VariableDeclaration
1721
+ exportPrefix = 'export '
1722
+ }
1723
+ }
1724
+
1725
+ if (!vd) continue
1726
+
1727
+ for (const decl of vd.declarations) {
1728
+ const d = decl as VariableDeclarator
1729
+ if (!d.init) continue
1730
+ const init = d.init as Expression
1731
+ const kind = (vd as unknown as { kind: string }).kind ?? 'const'
1732
+
1733
+ if ((d.id as unknown as { type: string }).type === 'Identifier') {
1734
+ // ── Simple binding: const name = <expr> ──────────────────────────
1735
+ const varName = (d.id as unknown as { name: string }).name
1736
+ if (!referencesAny(init, allNames, scope)) {
1737
+ const staticValue = evaluateStaticValue(init, scope, code)
1738
+ if (staticValue !== null && staticValue !== undefined || isStaticNullishExpression(init)) {
1739
+ ;(scope as Map<string, unknown>).set(varName, staticValue)
1740
+ }
1741
+ continue
1742
+ }
1743
+
1744
+ // const name = ref() — resolved by the pre-pass. Compile the (now
1745
+ // defined) ref combinator in place; codegen inlines the whole recursive
1746
+ // cluster behind a named function. The `.define(...)` statements are
1747
+ // removed separately.
1748
+ if (refNames.has(varName)) {
1749
+ const refEntry = scope.get(varName)
1750
+ const refCombi = refEntry?.combi ?? null
1751
+ if (refCombi) {
1752
+ // `mfSrcs` is the POSITIONAL fallback the evaluator collected while it
1753
+ // built this combinator. The encoder's def-carried sources win; this only
1754
+ // fills holes. Passing `undefined` here is what made a ref()'s reducers
1755
+ // print as `() => {}`.
1756
+ const compiled = compile(refCombi, refEntry?.mfSrcs, { recovery, coverage: grammarCoverage })
1757
+ if (compiled.inlineExpression === null) {
1758
+ warn(init.start, `"${varName}" is a ref() that couldn't be inlined (was .define() called with a static combinator?)`
1759
+ + reasonSuffix(compiled.runtimeOnly))
1760
+ continue
1761
+ }
1762
+ usedTableRuntime = true
1763
+ replacements.push({ start: init.start, end: init.end, replacement: compiled.inlineExpression })
1764
+ continue
1765
+ }
1766
+ }
1767
+
1768
+ // const name = rules(factory) → the ONE shared compiled-rule-map
1769
+ // expression, so `name.RuleX(...)` resolves to the compiled function
1770
+ // at runtime (the map's own values are already plain functions).
1771
+ if (isRulesCall(init)) {
1772
+ const compiledRules = compileRulesFactory(init, varName)
1773
+ if (!compiledRules) continue
1774
+ // Remember the rule map so a same-file `compose([varName, …])` can fuse it,
1775
+ // and its grammar-level trivia so that compose can adopt it (composing-wins).
1776
+ localRuleMaps.set(varName, compiledRules.ruleMap)
1777
+ if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia)
1778
+ if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip)
1779
+ // Carry the grammar's linkable pieces ON the value so a downstream package
1780
+ // composes it via `import { <name> }` alone. Needed when EXPORTED — and
1781
+ // computed unconditionally for a SHARED SHAPE, because the pieces are the
1782
+ // proof that the shape really did compile (see below).
1783
+ //
1784
+ // Thread `scanSkip` explicitly: it is what a downstream package composes.
1785
+ // (The `_meta` stamp in evaluateRulesFactory also covers it; passing it
1786
+ // here keeps the intent local and independent of that.) `trivia` is NOT
1787
+ // threaded — it is composing-wins, so the downstream compose supplies it;
1788
+ // `scanSkip` is per-piece (opaque units are dialect-specific) and must
1789
+ // travel WITH the grammar or the downstream loses ambient skipping.
1790
+ //
1791
+ // `compileLinkableTable` is the piece-artifact producer — the job this call
1792
+ // site actually has. It reports `external` as a first-class field, which is
1793
+ // what the SHARED-SHAPE check below needs, and it encodes a piece WITH holes
1794
+ // to `prog: null` while keeping its IR, so a hole is a described state rather
1795
+ // than a refusal.
1796
+ const ns = nsFor(varName)
1797
+ const pieces = exportPrefix || compiledRules.replacement === null
1798
+ ? compileLinkableTable([...compiledRules.ruleMap], ns, {
1799
+ ...(compiledRules.scanSkip ? { scanSkip: compiledRules.scanSkip } : {}),
1800
+ ...(compiledRules.trackLines ? { trackLines: true } : {}),
1801
+ recovery,
1802
+ })
1803
+ : null
1804
+ // A SHARED SHAPE — a map with an unresolved external `g.` ref, e.g.
1805
+ // `Ratio: sequence(g.Value, literal('/'), g.Value)` where the consuming
1806
+ // dialect defines `Value`. It can't be inlined as a standalone parser (the
1807
+ // hole has no body here), so its runtime value stays the `rules(…)` call —
1808
+ // the interpreter map, which is the only correct standalone value for a
1809
+ // grammar with a hole. `compileLinkable` DOES compile it (it emits a by-name
1810
+ // `_r_<Name>` call for each hole, bound at fuse time), so a downstream
1811
+ // `compose()` / `composeLeaf()` still fully macro-fuses it. Nothing failed,
1812
+ // so this doesn't warn — it only pins the macro import, since the emitted
1813
+ // source still calls `rules`. Requiring `pieces` here is what keeps this
1814
+ // from swallowing a map that failed to inline for some OTHER reason and
1815
+ // merely happens to also reference an external rule.
1816
+ if (compiledRules.replacement === null) {
1817
+ if (!pieces || pieces.external.length === 0) {
1818
+ warn(init.start, `${varName}: rule map couldn't be inlined` + reasonSuffix(compiledRules.refusals))
1819
+ // THE WHOLE MODULE FALLS BACK, for the same reason a runtime `compose()`
1820
+ // does. This map keeps its `rules(…)` source and is therefore built by the
1821
+ // INTERPRETER, from combinators — but its ambient trivia is an ordinary
1822
+ // declaration that this pass may already have lowered to a compiled rule
1823
+ // function. Mixing the two hands `rules()` a function where it needs a
1824
+ // combinator, and it throws walking it for reflection. Partial lowering is
1825
+ // only ever safe when every consumer of a lowered declaration also lowered.
1826
+ runtimeComposeFallback = true
1827
+ continue
1828
+ }
1829
+ keepMacroImport = true
1830
+ }
1831
+ const source = compiledRules.replacement ?? code.slice(init.start, init.end)
1832
+ // Carry only when the pieces are fully static (no runtime-only callbacks) —
1833
+ // otherwise the grammar isn't source-free composable and we ship it as a
1834
+ // plain map.
1835
+ let carriedMetadata: CarriedItem[] | undefined
1836
+ if (exportPrefix && pieces) {
1837
+ // Carry the compact IR. Thread the grammar's scanSkip into it so a
1838
+ // downstream compose of this imported grammar re-lowers its
1839
+ // scanTo/balanced sites ambiently.
1840
+ //
1841
+ // IR OR NOTHING. The old alternative was to carry the fully lowered pieces,
1842
+ // which a downstream compose could link but never RE-LOWER — so it silently
1843
+ // froze that grammar's trivia against any later composing-wins. With one
1844
+ // lowering there is no such artifact, and a grammar that cannot serialize
1845
+ // simply is not carried: an absent carried list makes the downstream compose
1846
+ // say so, where a frozen one did not.
1847
+ const ir = serializeRuleMap([...compiledRules.ruleMap] as never, compiledRules.scanSkip)
1848
+ if (ir) carriedMetadata = [{ ns, ir, ...(compiledRules.trackLines ? { trackLines: true as const } : {}) }]
1849
+ // NOT SILENT. An export with no carried IR cannot be composed downstream at
1850
+ // all — the consumer's `compose()` will fall back to runtime and say so, but
1851
+ // by then the cause is a module away. The source lowering carried fully
1852
+ // lowered pieces here instead; the table has no such form, because merging
1853
+ // two ALREADY-ENCODED programs means relocating code offsets and merging
1854
+ // every pool, which is the one route `compile-linkable-table.ts` documents
1855
+ // as deferred. Naming it at the origin is the least this can do.
1856
+ else warn(init.start, `${varName}: exported grammar could not be serialized to IR, so it carries no `
1857
+ + 'composable pieces — a downstream compose() of this grammar will fall back to runtime. '
1858
+ + 'Re-run with PARSEMAN_IR_DEBUG=1 to print the combinator that blocked serialization.')
1859
+ }
1860
+ const replacement = compiledRules.replacement !== null
1861
+ ? compiledRules.replacementWithMetadata!(staticTableMetadataSource({
1862
+ ...(carriedMetadata === undefined ? {} : { carried: carriedMetadata }),
1863
+ reflection: compiledRules.reflection,
1864
+ }))
1865
+ : (() => {
1866
+ const covered = withCoverageDefinitions(
1867
+ source,
1868
+ compiledRules.coverageDefinitions?.length
1869
+ ? compiledRules.coverageDefinitions
1870
+ : emittedCoverageDefinitions(source, `${id} rules()`),
1871
+ )
1872
+ return carriedMetadata === undefined ? covered : withCarriedPieces(covered, carriedMetadata)
1873
+ })()
1874
+ replacements.push({ start: init.start, end: init.end, replacement })
1875
+ if (compiledRules.replacement !== null) {
1876
+ markUsedImportedFactories(compiledRules.importedFactory ? [compiledRules.importedFactory] : undefined)
1877
+ }
1878
+ continue
1879
+ }
1880
+
1881
+ // const name = compose([...]) → STATIC fused source (eval-free); the macro
1882
+ // fuses at build, never emitting `new Function`.
1883
+ if (isComposeCall(init)) {
1884
+ const fused = compileComposeCall(init)
1885
+ if (!fused) {
1886
+ runtimeComposeFallback = true
1887
+ continue // leave the runtime compose() call in place
1888
+ }
1889
+ // Remember for a same-file downstream compose, and (if exported) carry the
1890
+ // re-lowerable list on the value so another package can compose this composed
1891
+ // grammar via `import { <name> }` (re-composition, no source) and re-lower it
1892
+ // under ITS composing trivia.
1893
+ localComposedCarried.set(varName, fused.carried)
1894
+ const replacement = exportPrefix ? fused.exportedReplacement : fused.replacement
1895
+ // Coverage definitions already ride in the table program's `cv` pool;
1896
+ // `tableRules` exposes that same pool through the metadata prototype.
1897
+ replacements.push({ start: init.start, end: init.end, replacement })
1898
+ markUsedImportedFactories(fused.importedFactories)
1899
+ continue
1900
+ }
1901
+
1902
+ // `composeLeaf([...recognition, localRules])` is static and terminal:
1903
+ // local direct builders stay lexical and are not carried/recomposable.
1904
+ if (isComposeLeafCall(init)) {
1905
+ const fused = compileComposeLeafCall(init)
1906
+ if (!fused) {
1907
+ const reasons = warnings.slice(-4)
1908
+ throw new Error(
1909
+ `${id}:${lineOf(init.start)} — composeLeaf() must macro-fuse; runtime composition is forbidden`
1910
+ + (reasons.length ? `\n causes:\n - ${reasons.join('\n - ')}` : ''),
1911
+ )
1912
+ }
1913
+ replacements.push({ start: init.start, end: init.end, replacement: fused.replacement })
1914
+ markUsedImportedFactories(fused.importedFactories)
1915
+ continue
1916
+ }
1917
+
1918
+ const mapFnSources: string[] = []
1919
+ const parser = evaluateExpr(init, scope, code, mapFnSources)
1920
+ if (parser === null) {
1921
+ const wordFactory = evaluateWordFactory(init, scope, code)
1922
+ if (wordFactory) {
1923
+ ;(scope as Map<string, unknown>).set(varName, wordFactory)
1924
+ replacements.push({ start: init.start, end: init.end, replacement: 'undefined' })
1925
+ continue
1926
+ }
1927
+ const whenFactory = evaluateWhenFactory(init, scope, code)
1928
+ if (whenFactory) {
1929
+ ;(scope as Map<string, unknown>).set(varName, whenFactory)
1930
+ replacements.push({ start: init.start, end: init.end, replacement: 'undefined' })
1931
+ continue
1932
+ }
1933
+ // A shared array-of-combinators const (e.g. a reusable `skip` set) — store
1934
+ // it in scope so `{ skip: name }` on a later scanTo/balanced resolves the
1935
+ // array. Left in the emitted output; it references other emitted consts.
1936
+ const combiArray = evaluateCombinatorArray(init, scope, code)
1937
+ if (combiArray) {
1938
+ ;(scope as Map<string, unknown>).set(varName, combiArray)
1939
+ continue
1940
+ }
1941
+ const staticValue = evaluateStaticValue(init, scope, code)
1942
+ if (staticValue !== null && staticValue !== undefined || isStaticNullishExpression(init)) {
1943
+ ;(scope as Map<string, unknown>).set(varName, staticValue)
1944
+ continue
1945
+ }
1946
+ // A `rules()` FACTORY is not a combinator and never was one — it is a function
1947
+ // that returns the rule map. It only reaches this branch now that a factory can
1948
+ // be shared by name (see `factoryDecls`), and warning here would tell the author
1949
+ // to "simplify" the one declaration the two-artifact pattern requires.
1950
+ //
1951
+ // Its text is left VERBATIM, which is fine for a local `const` — nothing
1952
+ // references it after lowering, so it is dead code the bundler drops. An
1953
+ // EXPORTED one cannot be dropped, so the macro-only identifiers in its body
1954
+ // (`node`, `sequence`, `literal`, …) survive into the artifact with nothing
1955
+ // binding them once the macro import is removed. Record it; whether that is
1956
+ // actually fatal depends on whether the import IS removed, which is only
1957
+ // decided once every declaration has been seen.
1958
+ if (factoryDecls.has(varName)) {
1959
+ if (exportPrefix) exportedFactories.push({ name: varName, pos: init.start })
1960
+ continue
1961
+ }
1962
+ warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`)
1963
+ continue
1964
+ }
1965
+
1966
+ // Sources are carried on each transform's def (set by the evaluator) AND
1967
+ // collected positionally into `mapFnSources`. Codegen only needed the former;
1968
+ // the TABLE ENCODER needs whichever is present, so hand it both — the def
1969
+ // sources win and the positional list fills holes. Passing `undefined` here
1970
+ // is what made every macro-lowered reducer print as `() => {}`.
1971
+ const compiled = compile(parser, mapFnSources, { recovery, coverage: grammarCoverage })
1972
+ if (compiled.inlineExpression === null) {
1973
+ warn(init.start, `"${varName}" couldn't be inlined (likely closes over a runtime value)`
1974
+ + reasonSuffix(compiled.runtimeOnly))
1975
+ continue
1976
+ }
1977
+
1978
+ usedTableRuntime = true
1979
+ replacements.push({
1980
+ start: init.start,
1981
+ end: init.end,
1982
+ replacement: compiled.inlineExpression,
1983
+ })
1984
+
1985
+ // Store enriched scope entry so factories can replay mfSrcs
1986
+ scope.set(varName, { combi: parser, mfSrcs: mapFnSources })
1987
+
1988
+ } else if ((d.id as unknown as { type: string }).type === 'ObjectPattern') {
1989
+ // ── Destructured binding: const { a, b } = rules(g => { ... }) ──
1990
+ // Only handle rules() factory calls
1991
+ if (!referencesAny(init, allNames, scope)) continue
1992
+ if (!isRulesCall(init)) {
1993
+ warn(init.start, `destructured macro binding must come from rules(...)`)
1994
+ continue
1995
+ }
1996
+
1997
+ const compiledRules = compileRulesFactory(init, '{ … }')
1998
+ if (!compiledRules) continue
1999
+ // A destructured binding names INDIVIDUAL rules, so it has to inline —
2000
+ // there is no shared-shape path here (a hole has no standalone value).
2001
+ if (compiledRules.replacement === null) { warn(init.start, `{ … }: rule map couldn't be inlined` + reasonSuffix(compiledRules.refusals)); continue }
2002
+
2003
+ // Walk the ObjectPattern properties, validating each destructured key
2004
+ // exists on the compiled rule map — collect bindings before emitting
2005
+ // any replacement text (uniform with the previous all-or-nothing
2006
+ // per-declaration behavior).
2007
+ const pattern = d.id as unknown as { properties: unknown[] }
2008
+ const bindings: Array<{ ruleKey: string; localName: string }> = []
2009
+ let allOk = true
2010
+
2011
+ for (const prop of pattern.properties) {
2012
+ const p = prop as { type: string; key: { type: string; name?: string; value?: unknown }; value: { type: string; name?: string } }
2013
+ if (p.type === 'RestElement' || p.type === 'BindingRestElement') {
2014
+ warn(init.start, `rest element in a rules() destructure isn't supported`)
2015
+ allOk = false; break
2016
+ }
2017
+
2018
+ const ruleKey = p.key.type === 'Identifier' ? p.key.name!
2019
+ : p.key.type === 'StringLiteral' ? String(p.key.value)
2020
+ : null
2021
+ const localName = (p.value.type === 'Identifier' || p.value.type === 'BindingIdentifier') ? p.value.name!
2022
+ : ruleKey
2023
+ if (!ruleKey || !localName) { allOk = false; break }
2024
+
2025
+ if (!compiledRules.ruleMap.has(ruleKey)) {
2026
+ warn(init.start, `destructured rule "${ruleKey}" isn't returned by the rules() factory`)
2027
+ allOk = false; break
2028
+ }
2029
+
2030
+ bindings.push({ ruleKey, localName })
2031
+ // Store under the local name so a later macro declaration can reference it.
2032
+ const rule = compiledRules.ruleMap.get(ruleKey)
2033
+ if (rule) scope.set(localName, { combi: rule, mfSrcs: [] })
2034
+ }
2035
+
2036
+ if (!allOk) continue
2037
+
2038
+ // ONE shared holder (not exported — an internal implementation
2039
+ // detail) evaluates the compiled rule map exactly once; each
2040
+ // destructured local name is just a property read off it, preserving
2041
+ // that name's own export-ness from the original declaration.
2042
+ const holderVar = `__rules${ruleMapHolderCounter++}`
2043
+ const lines = [
2044
+ `const ${holderVar} = ${compiledRules.replacement}`,
2045
+ ...bindings.map(({ ruleKey, localName }) =>
2046
+ `${exportPrefix}${kind} ${localName} = ${holderVar}[${JSON.stringify(ruleKey)}]`),
2047
+ ]
2048
+
2049
+ replacements.push({
2050
+ start: stmtStart,
2051
+ end: stmtEnd,
2052
+ replacement: lines.join('\n'),
2053
+ })
2054
+ markUsedImportedFactories(compiledRules.importedFactory ? [compiledRules.importedFactory] : undefined)
2055
+ }
2056
+ }
2057
+ }
2058
+
2059
+ // Nothing to rewrite and nothing to report — leave the file untouched.
2060
+ if (replacements.length === 0 && warnings.length === 0) return null
2061
+
2062
+ // If every declaration referencing an imported name was successfully inlined,
2063
+ // the import is no longer needed. Otherwise downgrade to runtime (strip just
2064
+ // the macro attribute so the import stays valid for the interpreter). A shared
2065
+ // shape also keeps the import — its emitted value still calls `rules(…)` — but
2066
+ // without the "fell back to the interpreter" warning, because nothing failed.
2067
+ for (const imp of macroImports) {
2068
+ imp.fullyResolved = !anyUnresolved && !keepMacroImport
2069
+ }
2070
+
2071
+ // An exported `rules()` factory whose macro import is about to be removed is an
2072
+ // UNRUNNABLE binding: lowering erases the call sites, but an export must still hold a
2073
+ // value, so the factory body ships verbatim naming `node`/`sequence`/`literal`/… that
2074
+ // nothing imports. It does not fail at build time and it does not fail at import time
2075
+ // — it throws `ReferenceError` the first time anything calls it, which in practice is
2076
+ // in a consumer's process, long after the artifact was published.
2077
+ //
2078
+ // There is no correct value to emit instead. Pinning the macro import would make the
2079
+ // binding run, but only by dragging the whole parseman runtime into an artifact whose
2080
+ // entire point is not to need it — a silent trade the author never asked for. So this
2081
+ // refuses to emit the shape at all. The fix is the author's: drop the `export`, since
2082
+ // a factory is macro-only by construction and no runtime consumer can use one.
2083
+ if (exportedFactories.length > 0 && macroImports.some(imp => imp.fullyResolved)) {
2084
+ const which = exportedFactories.map(f => ` - ${id}:${lineOf(f.pos)} — export const ${f.name}`).join('\n')
2085
+ throw new Error(
2086
+ `${id} — a rules() factory cannot be exported; lowering leaves its body verbatim, so the `
2087
+ + `export would ship a binding that throws ReferenceError on first call:\n${which}\n`
2088
+ + ` Drop the \`export\`. The factory is macro-only; \`rules(${exportedFactories[0]!.name})\` `
2089
+ + `is lowered in place and needs no runtime value.`,
2090
+ )
2091
+ }
2092
+
2093
+ const ms = new MagicString(code)
2094
+ const replacementRanges = runtimeComposeFallback
2095
+ ? []
2096
+ : replacements.map(({ start, end }) => ({ start, end }))
2097
+ const isInsideReplacement = (start: number, end: number): boolean =>
2098
+ replacementRanges.some(r => start >= r.start && end <= r.end)
2099
+ const importedBindingStillReferenced = (local: string): boolean => {
2100
+ const walk = (node: unknown, parent?: AnyNode, parentKey?: string): boolean => {
2101
+ if (!node || typeof node !== 'object') return false
2102
+ const rec = node as AnyNode
2103
+ if (rec.type === 'ImportDeclaration') return false
2104
+ if (typeof rec.start === 'number' && typeof rec.end === 'number' && isInsideReplacement(rec.start, rec.end)) return false
2105
+ if ((rec.type === 'Identifier' || rec.type === 'BindingIdentifier') && (rec as { name?: string }).name === local) {
2106
+ if (
2107
+ parentKey === 'key' &&
2108
+ (parent?.type === 'ObjectProperty' || parent?.type === 'Property' || parent?.type === 'PropertyDefinition') &&
2109
+ (parent as { computed?: boolean }).computed !== true
2110
+ ) return false
2111
+ if (parentKey === 'property' && parent?.type === 'StaticMemberExpression') return false
2112
+ if (parentKey === 'property' && parent?.type === 'MemberExpression' && (parent as { computed?: boolean }).computed !== true) return false
2113
+ return true
2114
+ }
2115
+ for (const key of Object.keys(rec)) {
2116
+ if (key === 'type' || key === 'start' || key === 'end') continue
2117
+ const value = (rec as Record<string, unknown>)[key]
2118
+ if (Array.isArray(value)) {
2119
+ if (value.some(child => walk(child, rec, key))) return true
2120
+ } else if (walk(value, rec, key)) {
2121
+ return true
2122
+ }
2123
+ }
2124
+ return false
2125
+ }
2126
+ return (body as unknown[]).some(stmt => walk(stmt))
2127
+ }
2128
+
2129
+ for (const imp of ordinaryImports) {
2130
+ if (
2131
+ !runtimeComposeFallback &&
2132
+ imp.specifiers.length > 0 &&
2133
+ imp.specifiers.every(spec =>
2134
+ spec.type === 'ImportSpecifier' &&
2135
+ usedImportedFactories.has(spec.local) &&
2136
+ !importedBindingStillReferenced(spec.local),
2137
+ )
2138
+ ) {
2139
+ ms.remove(imp.start, imp.end)
2140
+ }
2141
+ }
2142
+
2143
+ // Strip `x.define(...)` statements — only when every ref() cluster was inlined.
2144
+ // If anything fell back to the interpreter the import stays, and those
2145
+ // statements are still needed to wire the ref at runtime. (A shared shape holds
2146
+ // no ref() cluster of its own, so it doesn't gate this.)
2147
+ if (!anyUnresolved) {
2148
+ for (const { start, end } of defineRemovals) ms.remove(start, end)
2149
+ }
2150
+
2151
+ for (const imp of macroImports) {
2152
+ if (imp.fullyResolved) {
2153
+ ms.remove(imp.start, imp.end)
2154
+ } else {
2155
+ // Strip only the macro attribute, keep the import
2156
+ const original = code.slice(imp.start, imp.end)
2157
+ const stripped = original
2158
+ .replace(/\s+with\s*\{[^}]*\}/gs, '')
2159
+ .replace(/\s+assert\s*\{[^}]*\}/gs, '')
2160
+ ms.overwrite(imp.start, imp.end, stripped)
2161
+ }
2162
+ }
2163
+
2164
+ // Runtime compose consumes combinator objects. A partially lowered module mixes
2165
+ // compiled parser functions with those objects (for example `trivia(ws)` after
2166
+ // `ws` was lowered), so an unresolved compose makes the whole module runtime.
2167
+ const applied = runtimeComposeFallback ? [] : replacements
2168
+ // NO MODULE HOIST. It existed to deduplicate declarations across the fused IIFEs the
2169
+ // source lowering emitted — `_pfFail` and friends, one copy per variant. A table
2170
+ // replacement is a `tableRules(...)` call over a data literal: there are no emitted
2171
+ // function declarations to share, so there is nothing to hoist and no marker to
2172
+ // resolve.
2173
+ for (const { start, end, replacement } of applied.slice().sort((a, b) => b.start - a.start)) {
2174
+ ms.overwrite(start, end, replacement)
2175
+ }
2176
+
2177
+ // Stamp the generated artifact with a version-lock banner. This is the exact spot a
2178
+ // stale artifact is inspected, and the version here IS the stamp fusedBody's
2179
+ // version-lock assertion compares (see src/version.ts).
2180
+ // THE SHARED DRIVER. A table replacement names `tableRules` and nothing in the
2181
+ // consumer's module binds it — the macro import it might have come from is
2182
+ // exactly what lowering deletes. Emitted only when a table was actually applied,
2183
+ // so a module that lowered nothing (or fell back to runtime compose) does not
2184
+ // acquire an import it never uses, and so the artifact of a source-lowered
2185
+ // module is unchanged.
2186
+ if (usedTableRuntime && applied.length > 0) {
2187
+ ms.prepend(`import { tableRules } from ${JSON.stringify(TABLE_RUNTIME_SPECIFIER)}\n`)
2188
+ }
2189
+ if (applied.length > 0) {
2190
+ ms.prepend(
2191
+ `// Generated by parseman v${PARSEMAN_VERSION} — DO NOT EDIT.\n` +
2192
+ `// Version-locked: compile AND fuse/link this artifact with parseman v${PARSEMAN_VERSION} ONLY.\n` +
2193
+ `// Parseman does not read artifacts across versions; recompile if the version differs.\n`,
2194
+ )
2195
+ }
2196
+
2197
+ /*
2198
+ * REFUSE TO EMIT a module that names something nothing binds.
2199
+ *
2200
+ * Every deletion lowering performs — the `rules(…)` call sites, the `x.define(…)`
2201
+ * statements, and last the macro import — is only safe because the text that read
2202
+ * the deleted binding went with it. Where a shape slips through where it did not,
2203
+ * the artifact builds clean, imports clean, and throws `ReferenceError` the first
2204
+ * time a consumer calls the binding: jess shipped exactly that for three days
2205
+ * across three grammars, 26 undefined identifiers in the css parser alone, and had
2206
+ * to write this check itself downstream. It belongs here.
2207
+ *
2208
+ * This is the NET, not the diagnostic. Shapes we recognise are refused where they
2209
+ * are recognised, with an error that says what to do — an exported `rules()`
2210
+ * factory throws above, before this point, so it is never double-reported. What
2211
+ * reaches here is a name that escaped by a route nobody enumerated, which is the
2212
+ * whole reason for checking the emitted text instead of guessing at shapes.
2213
+ *
2214
+ * Gated on `applied.length > 0` — the same condition as the version banner. A
2215
+ * module that lowered nothing had nothing deleted from it, so it cannot have
2216
+ * acquired a free name, and the scan (one extra parse of the emitted module, at
2217
+ * macro time) is not worth paying for. Nothing here runs in, or is emitted into,
2218
+ * the artifact.
2219
+ */
2220
+ if (applied.length > 0) {
2221
+ /*
2222
+ * Only names lowering MADE free. A module that already read something nothing binds
2223
+ * — a host that injects the name some other way, an ambient the author knows about
2224
+ * — was written that way, and reporting it here would be parseman failing a build
2225
+ * over a decision it had no part in. Subtracting the source's own free names is what
2226
+ * keeps this a check on parseman's output rather than a lint on the author's input.
2227
+ * The source scan runs ONLY when the emitted module already looks wrong, so the
2228
+ * clean path costs one parse, not two.
2229
+ */
2230
+ let sourceFree: Set<string> | null = null
2231
+ const freeInSource = (): Set<string> =>
2232
+ sourceFree ??= new Set(findFreeIdentifiers(code, id).map(f => f.name))
2233
+ const free = findFreeIdentifiers(ms.toString(), id)
2234
+ .filter(f => !freeInSource().has(f.name))
2235
+ if (free.length > 0) {
2236
+ const macroProvided = free.filter(f => allNames.has(f.name))
2237
+ const where = (f: (typeof free)[number]): string =>
2238
+ ` - ${id}:${f.line}:${f.column} — \`${f.name}\``
2239
+ + (f.enclosing ? `, inside \`${f.enclosing}\`` : '')
2240
+ const cause = macroProvided.length > 0
2241
+ ? `\n ${macroProvided.length === free.length ? 'All' : `${macroProvided.length}`} of these came from the `
2242
+ + `\`with { type: 'macro' }\` import, which was removed because every macro declaration lowered. `
2243
+ + `Something still names them verbatim — a declaration parseman left as text rather than compiling. `
2244
+ + `Make that declaration macro-buildable, or stop exporting it so lowering can drop it.`
2245
+ : ''
2246
+ throw new Error(
2247
+ `${id} — parseman will not emit this module: ${free.length} identifier(s) are read but bound by `
2248
+ + `nothing, so it would throw ReferenceError at runtime (positions are in the EMITTED module):\n`
2249
+ + `${free.map(where).join('\n')}${cause}`,
2250
+ )
2251
+ }
2252
+ }
2253
+
2254
+ // NOTE: `warnUnloweredRegex` and the inline-expansion cap were both properties of the
2255
+ // SOURCE lowering — "this regex became RegExp.exec instead of a charCodeAt scan", and
2256
+ // "this emitted function hit INLINE_MAX_NODES". The table has no emitted function to
2257
+ // bound and no per-regex emission choice to report, so both diagnostics are gone
2258
+ // rather than reporting a value nothing computes. The option is still accepted so a
2259
+ // caller's config does not break; it is now inert.
2260
+
2261
+ setReducerResolver(null)
2262
+ // Every place the compiler chose a correct-but-slower path for this module. Reported
2263
+ // on the ordinary warning channel and greppable on `[parseman] degraded`, so a
2264
+ // consumer's build gate can assert zero of them the way jess's `check:macro` already
2265
+ // asserts zero `"falling back to runtime"` lines. `PARSEMAN_DEGRADATION=error` turns
2266
+ // the assertion on here instead of in the consumer.
2267
+ const degradations = endDegradationCapture()
2268
+ if (degradations.length > 0) {
2269
+ const lines = formatDegradations(degradations)
2270
+ if (resolveDegradationLevel() === 'error') {
2271
+ throw new Error(`parseman: ${degradations.length} degraded compilation path(s) in ${id}\n${lines.join('\n')}`)
2272
+ }
2273
+ for (const l of lines) warnings.push(`${id}: ${l}`)
2274
+ }
2275
+
2276
+ return {
2277
+ code: ms.toString(),
2278
+ map: ms.generateMap({ hires: true }),
2279
+ warnings,
2280
+ }
2281
+ }