parseman 0.46.0 → 0.47.1

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 +10339 -7787
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +10339 -7787
  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 +10924 -8506
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10924 -8506
  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 +45 -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 +9290 -7181
  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 +9300 -7191
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +313 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +313 -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 +11175 -9534
  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 +11175 -9534
  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 +2934 -1611
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2934 -1611
  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 +11463 -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 +11440 -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 +453 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +496 -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 +26 -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 +348 -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 +237 -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 +852 -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 +3006 -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 +1777 -0
  286. package/src/table/emit.ts +429 -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 +1680 -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 +467 -0
  295. package/src/table/program.ts +876 -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,1505 @@
1
+ /**
2
+ * Statically evaluates parseman combinator call expressions from an oxc AST
3
+ * into actual Combinator<unknown> objects by calling the real library functions.
4
+ *
5
+ * Returns null for anything unresolvable (external variables, template literals,
6
+ * computed keys, etc.) — callers leave those as-is.
7
+ */
8
+ import type {
9
+ Expression, Node,
10
+ ArrowFunctionExpression, Function as OxcFunction,
11
+ ReturnStatement,
12
+ VariableDeclaration, VariableDeclarator,
13
+ StaticMemberExpression,
14
+ ObjectExpression, ObjectProperty,
15
+ } from '@oxc-project/types'
16
+ import type { Combinator } from '../types.ts'
17
+ import type { DispatchArm } from '../combinators/dispatch.ts'
18
+ import { ref } from '../combinators/ref.ts'
19
+ import { rules, type RulesOptions } from '../combinators/parser.ts'
20
+ import * as parseman from '../index.ts'
21
+ import { directBuilderUnsupportedBindings } from './direct-builder-static.ts'
22
+ import type { ReducerResolver } from './reducer-resolver.ts'
23
+
24
+ /**
25
+ * Emit an AST subtree's source with TypeScript-only syntax removed. A gate source
26
+ * is sliced verbatim from the grammar's `.ts` and may carry a type annotation (e.g.
27
+ * `(s: any) => …`, unavoidable for a gate under a `g: any` factory with
28
+ * noImplicitAny). Where the macro INLINES the source, downstream TS→JS transpilation
29
+ * strips that — but a gated choice also round-trips through the `serializeRuleMap`
30
+ * IR string, which is re-lowered with `new Function` VERBATIM, where TS syntax is a
31
+ * hard parse error. So blank out every TS-only span (param/return/variable type
32
+ * annotations, generic type args, and `as`/`satisfies`/`!` cast suffixes) using the
33
+ * spans the oxc parser already gave us — no extra transpiler dependency. A subtree
34
+ * with no TS syntax (every existing untyped callback) is returned byte-for-byte, so
35
+ * standalone codegen output is unchanged.
36
+ */
37
+ function stripTsFromSource(node: Node, code: string): string {
38
+ const cuts: Array<[number, number]> = []
39
+ const walk = (n: unknown): void => {
40
+ if (!n || typeof n !== 'object') return
41
+ const rec = n as Record<string, unknown> & { type?: string; start?: number; end?: number; expression?: { start?: number; end?: number } }
42
+ // A whole TS-only node (a type annotation, type-argument list, etc.): drop it.
43
+ if (typeof rec.type === 'string' && rec.type.startsWith('TS') && typeof rec.start === 'number' && typeof rec.end === 'number') {
44
+ const ex = rec.expression
45
+ // SUFFIX wrappers keep their expression; only the trailing TS is dropped:
46
+ // `x as T` / `x satisfies T` / `x!` (after the expression) and `f<T>` (the
47
+ // `<T>` type-argument list after the callee expression).
48
+ if ((rec.type === 'TSAsExpression' || rec.type === 'TSSatisfiesExpression' || rec.type === 'TSNonNullExpression' || rec.type === 'TSInstantiationExpression') && ex && typeof ex.end === 'number') {
49
+ cuts.push([ex.end, rec.end])
50
+ walk(rec.expression)
51
+ return
52
+ }
53
+ // PREFIX wrapper: `<T>x` (angle-bracket assertion) — cut the leading `<T>`,
54
+ // keep the wrapped expression.
55
+ if (rec.type === 'TSTypeAssertion' && ex && typeof ex.start === 'number') {
56
+ cuts.push([rec.start, ex.start])
57
+ walk(rec.expression)
58
+ return
59
+ }
60
+ // Everything else (annotations, bare type-argument lists) is dropped whole.
61
+ cuts.push([rec.start, rec.end])
62
+ return
63
+ }
64
+ for (const key of Object.keys(rec)) {
65
+ if (key === 'type' || key === 'start' || key === 'end') continue
66
+ const v = rec[key]
67
+ if (Array.isArray(v)) { for (const item of v) walk(item) }
68
+ else if (v && typeof v === 'object') walk(v)
69
+ }
70
+ }
71
+ walk(node)
72
+ const start = (node as { start: number }).start
73
+ const end = (node as { end: number }).end
74
+ if (cuts.length === 0) return code.slice(start, end)
75
+ cuts.sort((a, b) => a[0] - b[0])
76
+ let out = ''
77
+ let cur = start
78
+ for (const [s, e] of cuts) {
79
+ if (s < cur) continue // nested cut already covered
80
+ out += code.slice(cur, s)
81
+ cur = e
82
+ }
83
+ return out + code.slice(cur, end)
84
+ }
85
+
86
+ /**
87
+ * The stand-in for a `withCtx(extra, …)` argument the macro could not evaluate.
88
+ *
89
+ * It is a CLASS INSTANCE on purpose. `{}` would be indistinguishable from an
90
+ * author's own empty state object, so the table encoder would intern it, print
91
+ * it, and ship a grammar whose every state gate is silently false. A non-plain
92
+ * prototype fails `emittableConst`, which is what turns "we don't know the state"
93
+ * into a named `runtimeOnly` refusal instead of a wrong artifact.
94
+ */
95
+ class UnevaluatedExtra {}
96
+ const UNEVALUATED_EXTRA: unknown = new UnevaluatedExtra()
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Reducer resolution
100
+ //
101
+ // `buildSrc` is the source text of the EXPRESSION at the `node(...)` call site, so a
102
+ // reducer passed as a bare identifier — `node('Foo', p, { build: foldOperation })` —
103
+ // arrives as the string `"foldOperation"`. That matches no parameter list, so
104
+ // `confirmedBuildArity` returned `null` and every capture tier stayed on: the runtime
105
+ // cost of a rule depended on how its reducer was SPELLED.
106
+ //
107
+ // `reducer-resolver.ts` does the real work — lexical scope analysis over this module,
108
+ // plus cross-module import following — and this is where its answer is attached. The
109
+ // resolved arity lands on `_def.buildArity` and the resolved source on
110
+ // `_def.buildSigSrc`; both are ANALYSIS-ONLY and never emitted, so the generated builder
111
+ // reference is byte-identical either way.
112
+ // ---------------------------------------------------------------------------
113
+ let _reducers: ReducerResolver | null = null
114
+
115
+ /**
116
+ * The source of the module actually being EMITTED, so a factory evaluated out of some
117
+ * OTHER file can be told apart from one written here. See `setReducerResolver`.
118
+ */
119
+ let _entrySource: string | null = null
120
+
121
+ /** Install (or clear, with `null`) the resolver for the module being transformed. */
122
+ export function setReducerResolver(r: ReducerResolver | null, entrySource: string | null = null): void {
123
+ _reducers = r
124
+ _entrySource = entrySource
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Scope types
129
+ //
130
+ // Each scope entry is either a raw Combinator, or an enriched entry that
131
+ // carries the mapFnSources this combinator will contribute when the codegen
132
+ // traverses its subtree. The enriched form is needed so that anyValue can
133
+ // "replay" those sources when the combinator is referenced by another
134
+ // expression — keeping mapFnSources aligned with what ctx.mapFns builds.
135
+ // ---------------------------------------------------------------------------
136
+ export type ScopeEntry = {
137
+ combi: Combinator<unknown>
138
+ mfSrcs: string[]
139
+ }
140
+ export type Scope = Map<string, ScopeEntry>
141
+
142
+ // Internal XScope also holds non-Combinator values (g proxy objects etc.)
143
+ type XScopeVal = ScopeEntry | unknown
144
+ type XScope = Map<string, XScopeVal>
145
+
146
+ type WordFactoryEntry = { tag: 'wordFactory'; boundary: string; caseInsensitive: boolean }
147
+ type WhenFactoryEntry = { tag: 'whenFactory'; caseInsensitive: boolean }
148
+ type StaticValueEntry = { value: unknown; mfSrcs: string[] }
149
+
150
+ function isWordFactory(v: unknown): v is WordFactoryEntry {
151
+ return !!v && typeof v === 'object' && (v as WordFactoryEntry).tag === 'wordFactory'
152
+ }
153
+
154
+ function isWhenFactory(v: unknown): v is WhenFactoryEntry {
155
+ return !!v && typeof v === 'object' && (v as WhenFactoryEntry).tag === 'whenFactory'
156
+ }
157
+
158
+ function isStaticValueEntry(v: unknown): v is StaticValueEntry {
159
+ return !!v && typeof v === 'object' && 'value' in v && 'mfSrcs' in v
160
+ }
161
+
162
+ function wordFactoryFromArgs(args: readonly (Expression | { type: 'SpreadElement' })[], scope: XScope, code?: string, mfs?: string[]): WordFactoryEntry | null {
163
+ const [boundaryOrOptsArg, optsArg] = args
164
+ if (boundaryOrOptsArg?.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
165
+
166
+ const rawBoundaryOrOpts = boundaryOrOptsArg === undefined
167
+ ? undefined
168
+ : anyValue(boundaryOrOptsArg as Expression, scope, code, mfs)
169
+ const boundaryOrOpts = rawBoundaryOrOpts === undefined ? '_0-9A-Za-z' : rawBoundaryOrOpts
170
+ const opts = optsArg === undefined
171
+ ? undefined
172
+ : anyValue(optsArg as Expression, scope, code, mfs)
173
+
174
+ if (typeof boundaryOrOpts === 'string') {
175
+ if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) return null
176
+ const caseInsensitive = typeof opts === 'object' && opts !== null && 'caseInsensitive' in opts
177
+ ? (opts as { caseInsensitive?: unknown }).caseInsensitive
178
+ : false
179
+ if (typeof caseInsensitive !== 'boolean') return null
180
+ return { tag: 'wordFactory', boundary: boundaryOrOpts, caseInsensitive }
181
+ }
182
+
183
+ if (typeof boundaryOrOpts !== 'object' || boundaryOrOpts === null || Array.isArray(boundaryOrOpts) || opts !== undefined) {
184
+ return null
185
+ }
186
+ const caseInsensitive = 'caseInsensitive' in boundaryOrOpts
187
+ ? (boundaryOrOpts as { caseInsensitive?: unknown }).caseInsensitive
188
+ : false
189
+ if (typeof caseInsensitive !== 'boolean') return null
190
+ return { tag: 'wordFactory', boundary: '_0-9A-Za-z', caseInsensitive }
191
+ }
192
+
193
+ function dispatchWhenOptions(v: unknown): parseman.DispatchWhenOptions | null {
194
+ if (v === undefined) return {}
195
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) return null
196
+ for (const key of Object.keys(v)) {
197
+ if (key !== 'caseInsensitive') return null
198
+ }
199
+ const caseInsensitive = 'caseInsensitive' in v
200
+ ? (v as { caseInsensitive?: unknown }).caseInsensitive
201
+ : false
202
+ if (typeof caseInsensitive !== 'boolean') return null
203
+ return { caseInsensitive }
204
+ }
205
+
206
+ function whenFactoryFromArgs(args: readonly (Expression | { type: 'SpreadElement' })[], scope: XScope, code?: string, mfs?: string[]): WhenFactoryEntry | null {
207
+ const [optsArg, extra] = args
208
+ if (optsArg?.type === 'SpreadElement' || extra !== undefined) return null
209
+ const rawOpts = optsArg === undefined
210
+ ? undefined
211
+ : anyValue(optsArg as Expression, scope, code, mfs)
212
+ const opts = dispatchWhenOptions(rawOpts)
213
+ return opts === null ? null : { tag: 'whenFactory', caseInsensitive: opts.caseInsensitive ?? false }
214
+ }
215
+
216
+ /**
217
+ * Generic fallback table, consulted LAST (see the dispatch at the end of
218
+ * `exprToCombi`). A combinator with an UNCONDITIONAL explicit branch above must
219
+ * not appear here — the entry would be unreachable. `many`/`oneOrMore`/`sepBy`/
220
+ * `oneOrMoreSep`/`peek`/`not` are all handled explicitly, because their emitters
221
+ * traverse the item more than once (mfSrcs replay) or take options that must not
222
+ * be silently dropped.
223
+ */
224
+ const SUPPORTED: Record<string, (...args: unknown[]) => Combinator<unknown>> = {
225
+ literal: (...a) => parseman.literal(a[0] as string, a[1] as parseman.LiteralOptions | undefined),
226
+ regex: (...a) => parseman.regex(a[0] as RegExp, a[1] as string | undefined),
227
+ keywords: (...a) => parseman.keywords(a[0] as readonly string[], a[1] as parseman.KeywordsOptions | undefined),
228
+ word: (...a) => parseman.word(a[0] as string, a[1] as string | undefined, a[2] as Omit<parseman.KeywordsOptions, 'boundary'> | undefined),
229
+ sequence: (...a) => (parseman.sequence as (...p: Combinator<unknown>[]) => Combinator<unknown[]>)(...(a as Combinator<unknown>[])),
230
+ choice: (...a) => (parseman.choice as (...p: Combinator<unknown>[]) => Combinator<unknown>)(...(a as Combinator<unknown>[])),
231
+ attempt: (...a) => parseman.attempt(a[0] as Combinator<unknown>),
232
+ optional: (...a) => parseman.optional(a[0] as Combinator<unknown>),
233
+ trivia: (...a) => parseman.trivia(a[0] as Combinator<unknown>),
234
+ classifiedTrivia: (...a) =>
235
+ parseman.classifiedTrivia(
236
+ a[0] as Readonly<Record<string, Combinator<unknown>>>,
237
+ ),
238
+ label: (...a) => parseman.label(a[0] as string, a[1] as Combinator<unknown>),
239
+ field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>),
240
+ noTrivia: (...a) => parseman.noTrivia(a[0] as Combinator<unknown>),
241
+ token: (...a) => parseman.token(a[0] as Combinator<unknown>),
242
+ // `routed(fallback)` — the fallback must be forwarded. A zero-arg entry here read
243
+ // as "routed takes no arguments" and SILENTLY built a bare `routed()`, so a
244
+ // production written to work both inside and outside a dispatch branch lost its
245
+ // out-of-branch behaviour under the macro while keeping it under the interpreter.
246
+ // `undefined` reproduces the bare def exactly (see `routed()`), so `routed()` is
247
+ // byte-identical.
248
+ routed: (...a) => parseman.routed(a[0] as Combinator<unknown> | undefined),
249
+ leaf: (...a) => parseman.leaf(a[0] as Combinator<unknown>, a[1] as (value: unknown, span: { start: number; end: number }) => unknown),
250
+ expect: (...a) => parseman.expect(a[0] as Combinator<unknown>, a[1] as string | undefined),
251
+ // Adjacency assertions carry only plain data (a polarity and an optional list of
252
+ // category names), so the macro reproduces them exactly — no source capture, no
253
+ // interpreter fallback.
254
+ adjacent: () => parseman.adjacent(),
255
+ notAdjacent: (...a) => parseman.notAdjacent(a[0] as { kinds?: readonly string[] } | undefined),
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Internal helpers
260
+ // ---------------------------------------------------------------------------
261
+
262
+ function isScopeEntry(v: unknown): v is ScopeEntry {
263
+ return !!v && typeof v === 'object' && 'combi' in v && 'mfSrcs' in v
264
+ }
265
+
266
+ /** THE reader for an object-literal property key, for every consumer in the plugin.
267
+ *
268
+ * Returns the key a JavaScript engine would use, or null when the property does not
269
+ * name a static key at all — a spread, a rest, or a COMPUTED key whose value is not
270
+ * known until runtime.
271
+ *
272
+ * Both halves are load-bearing and each was got wrong somewhere:
273
+ * - a quoted key is a `Literal`, not an `Identifier`. Reading only `key.name` sees
274
+ * `{ 'hostMode': 'cst' }` as having no hostMode, and drops the option SILENTLY.
275
+ * - `key.name` is also populated for a COMPUTED key `{ [hostMode]: … }`, where the
276
+ * identifier is a variable and the actual key is its value. Reading only
277
+ * `key.name` there invents an option the source never set.
278
+ *
279
+ * So an Identifier-only reader both misses keys that are present and matches keys that
280
+ * are not. Three option readers in plugin/index.ts each re-derived one wrong half; the
281
+ * fix is this one function, imported. */
282
+ export function propName(p: { type?: string; computed?: boolean; key?: unknown }): string | null {
283
+ if ((p.type !== undefined && p.type !== 'Property') || p.computed || !p.key) return null
284
+ const key = p.key as { type?: string; name?: unknown; value?: unknown }
285
+ return key.type === 'Identifier' ? (typeof key.name === 'string' ? key.name : null)
286
+ : key.type === 'Literal' ? String(key.value)
287
+ : null
288
+ }
289
+
290
+ /** Is this a gated-choice arm object literal — `{ gate, combinator }`? */
291
+ function isGatedArmExpr(e: { type: string }): boolean {
292
+ if (e.type !== 'ObjectExpression') return false
293
+ let hasGate = false, hasCombinator = false
294
+ for (const prop of (e as ObjectExpression).properties) {
295
+ if (prop.type !== 'Property') continue
296
+ const name = propName(prop as unknown as ObjectProperty)
297
+ if (name === 'gate') hasGate = true
298
+ else if (name === 'combinator') hasCombinator = true
299
+ }
300
+ return hasGate && hasCombinator
301
+ }
302
+
303
+ /** Extract the `gate` / `combinator` value expressions from a gated-arm object.
304
+ * Returns null on any unexpected shape (spread, computed key, extra key). */
305
+ function gatedArmParts(e: ObjectExpression): { gate: Expression; combinator: Expression } | null {
306
+ let gate: Expression | undefined
307
+ let combinator: Expression | undefined
308
+ for (const prop of e.properties) {
309
+ if (prop.type !== 'Property') return null
310
+ const op = prop as unknown as ObjectProperty
311
+ const name = propName(op)
312
+ if (name === 'gate') gate = op.value as Expression
313
+ else if (name === 'combinator') combinator = op.value as Expression
314
+ else return null
315
+ }
316
+ return gate && combinator ? { gate, combinator } : null
317
+ }
318
+
319
+ function dispatchArmValue(node: Expression, scope: XScope, code?: string, mfs?: string[]): DispatchArm<unknown> | null {
320
+ if (node.type === 'Identifier') {
321
+ const value = anyValue(node, scope, code, mfs)
322
+ return isDispatchArm(value) ? value : null
323
+ }
324
+
325
+ if (node.type !== 'CallExpression') return null
326
+ const callee = node.callee
327
+ if (callee.type !== 'Identifier') return null
328
+
329
+ const factory = scope.get(callee.name)
330
+ if (isWhenFactory(factory)) {
331
+ const [keyArg, parserArg, extraArg] = node.arguments
332
+ if (!keyArg || !parserArg || extraArg !== undefined || keyArg.type === 'SpreadElement' || parserArg.type === 'SpreadElement') return null
333
+ const key = anyValue(keyArg as Expression, scope, code, mfs)
334
+ const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
335
+ if (!isCombinator(parserValue)) return null
336
+ const opts = { caseInsensitive: factory.caseInsensitive }
337
+ if (typeof key === 'string') return parseman.when(key, parserValue, opts)
338
+ if (Array.isArray(key) && key.every(item => typeof item === 'string')) return parseman.when(key, parserValue, opts)
339
+ // `makeWhen(opts)` returns `(key, parser) => when(key, parser, opts)`, so it accepts
340
+ // EVERY key `when` accepts — including a startsWith/endsWith/matches matcher. Omitting
341
+ // that case here made the aliased form a hard macro failure ("factory isn't statically
342
+ // evaluable") for an arm the interpreter builds, so the alias silently carried a
343
+ // narrower contract than the constructor it forwards to.
344
+ if (isDispatchMatcher(key)) return parseman.when(key, parserValue, opts)
345
+ return null
346
+ }
347
+
348
+ if (callee.name === 'otherwise') {
349
+ const [parserArg] = node.arguments
350
+ if (!parserArg || parserArg.type === 'SpreadElement') return null
351
+ const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
352
+ if (!isCombinator(parserValue)) return null
353
+ return parseman.otherwise(parserValue)
354
+ }
355
+
356
+ if (callee.name === 'when') {
357
+ const [keyArg, parserArg, optsArg] = node.arguments
358
+ if (!keyArg || !parserArg || keyArg.type === 'SpreadElement' || parserArg.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
359
+ const key = anyValue(keyArg as Expression, scope, code, mfs)
360
+ const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
361
+ const opts = dispatchWhenOptions(optsArg === undefined ? undefined : anyValue(optsArg as Expression, scope, code, mfs))
362
+ if (opts === null) return null
363
+ if (!isCombinator(parserValue)) return null
364
+ if (typeof key === 'string') return parseman.when(key, parserValue, opts)
365
+ if (Array.isArray(key) && key.every(item => typeof item === 'string')) return parseman.when(key, parserValue, opts)
366
+ if (isDispatchMatcher(key)) return parseman.when(key, parserValue, opts)
367
+ }
368
+
369
+ return null
370
+ }
371
+
372
+ function isCombinator(v: unknown): v is Combinator<unknown> {
373
+ return !!v && typeof v === 'object' && '_def' in v
374
+ }
375
+
376
+ function isDispatchArm(v: unknown): v is DispatchArm<unknown> {
377
+ if (!v || typeof v !== 'object') return false
378
+ const rec = v as { kind?: unknown; keys?: unknown; matcher?: unknown; parser?: unknown; caseInsensitive?: unknown }
379
+ if (rec.kind === 'otherwise') return isCombinator(rec.parser)
380
+ if (rec.kind === 'whenMatcher') {
381
+ return isDispatchMatcher(rec.matcher) &&
382
+ typeof rec.caseInsensitive === 'boolean' &&
383
+ isCombinator(rec.parser)
384
+ }
385
+ return rec.kind === 'when' &&
386
+ Array.isArray(rec.keys) &&
387
+ rec.keys.every(key => typeof key === 'string') &&
388
+ typeof rec.caseInsensitive === 'boolean' &&
389
+ isCombinator(rec.parser)
390
+ }
391
+
392
+ function isDispatchMatcher(v: unknown): v is ReturnType<typeof parseman.startsWith> {
393
+ if (!v || typeof v !== 'object') return false
394
+ const rec = v as { kind?: unknown; value?: unknown; flags?: unknown }
395
+ if ((rec.kind === 'startsWith' || rec.kind === 'endsWith') && typeof rec.value === 'string') return true
396
+ return rec.kind === 'matches' &&
397
+ typeof rec.value === 'string' &&
398
+ typeof rec.flags === 'string'
399
+ }
400
+
401
+ /**
402
+ * Resolve an identifier from scope.
403
+ * If the entry carries mfSrcs, replay them into `mfs` so that the
404
+ * overall accumulator stays aligned with what codegen will push.
405
+ */
406
+ function scopeGet(scope: XScope, name: string, mfs?: string[]): Combinator<unknown> | null {
407
+ const entry = scope.get(name)
408
+ if (!entry) return null
409
+ if (isScopeEntry(entry)) {
410
+ if (mfs && entry.mfSrcs.length > 0) mfs.push(...entry.mfSrcs)
411
+ return entry.combi
412
+ }
413
+ if (isCombinator(entry)) return entry
414
+ return null
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Core evaluators
419
+ // ---------------------------------------------------------------------------
420
+
421
+ /** Read static node opts that affect generated grammar shape. */
422
+ function unwrapStaticExpr<T extends { type?: string }>(expr: T): T {
423
+ let cur = expr as unknown as { type?: string; expression?: T }
424
+ while (cur.type === 'TSAsExpression'
425
+ || cur.type === 'TSSatisfiesExpression'
426
+ || cur.type === 'TSNonNullExpression'
427
+ || cur.type === 'TSTypeAssertion'
428
+ || cur.type === 'TSInstantiationExpression'
429
+ || cur.type === 'ParenthesizedExpression') {
430
+ if (!cur.expression) break
431
+ cur = cur.expression as unknown as typeof cur
432
+ }
433
+ return cur as unknown as T
434
+ }
435
+
436
+ function staticLiteralValue(expr: unknown): unknown {
437
+ const val = unwrapStaticExpr(expr as { type?: string; value?: unknown })
438
+ return val.type === 'Literal' || val.type === 'BooleanLiteral' || val.type === 'NumericLiteral'
439
+ ? val.value
440
+ : undefined
441
+ }
442
+
443
+ function staticStringArray(expr: unknown, scope?: XScope): readonly string[] | undefined {
444
+ const id = unwrapStaticExpr(expr as { type?: string; name?: string })
445
+ if (id.type === 'Identifier' && scope !== undefined && id.name !== undefined) {
446
+ const scoped = scope.get(id.name)
447
+ const value = isStaticValueEntry(scoped) ? scoped.value : scoped
448
+ return Array.isArray(value) && value.every(v => typeof v === 'string') ? value : undefined
449
+ }
450
+ const arr = id as { type?: string; elements?: unknown[] }
451
+ if (arr.type !== 'ArrayExpression' || !Array.isArray(arr.elements)) return undefined
452
+ const out: string[] = []
453
+ for (const el of arr.elements) {
454
+ if (!el || (el as { type?: string }).type === 'SpreadElement') return undefined
455
+ const value = staticLiteralValue(el)
456
+ if (typeof value !== 'string') return undefined
457
+ out.push(value)
458
+ }
459
+ return out
460
+ }
461
+
462
+ type StaticNodeProject = { ok: true; value: number } | { ok: false }
463
+ const STATIC_NODE_OPTIONS_FAILED = Symbol('parseman.staticNodeOptions.failed')
464
+ const STATIC_NODE_OPTIONS_NOT_OPTIONS = Symbol('parseman.staticNodeOptions.notOptions')
465
+ type StaticNodeOptions =
466
+ | parseman.NodeOptions<readonly string[]>
467
+ | undefined
468
+ | typeof STATIC_NODE_OPTIONS_FAILED
469
+ | typeof STATIC_NODE_OPTIONS_NOT_OPTIONS
470
+
471
+ function staticNodeProject(expr: Expression): StaticNodeProject | undefined {
472
+ const literalValue = staticLiteralValue(expr)
473
+ if (typeof literalValue === 'number' && Number.isInteger(literalValue) && literalValue >= 0) {
474
+ return { ok: true, value: literalValue }
475
+ }
476
+ if (typeof literalValue === 'number' || literalValue !== undefined) return { ok: false }
477
+ return { ok: false }
478
+ }
479
+
480
+ function scopedStaticValue(expr: Expression, scope: XScope): { found: true; value: unknown } | { found: false } {
481
+ const unwrapped = unwrapStaticExpr(expr)
482
+ if (unwrapped.type !== 'Identifier' || unwrapped.name === 'undefined') return { found: false }
483
+ if (!scope.has(unwrapped.name)) return { found: false }
484
+ const scoped = scope.get(unwrapped.name)
485
+ const value = isStaticValueEntry(scoped) ? scoped.value : scoped
486
+ return { found: true, value }
487
+ }
488
+
489
+ function staticNodeOptionsFromValue(value: unknown): parseman.NodeOptions<readonly string[]> | undefined | typeof STATIC_NODE_OPTIONS_FAILED | typeof STATIC_NODE_OPTIONS_NOT_OPTIONS {
490
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || isCombinator(value)) return STATIC_NODE_OPTIONS_NOT_OPTIONS
491
+ const opts: parseman.NodeOptions<readonly string[]> = {}
492
+ const rec = value as Record<string, unknown>
493
+ for (const name of Object.keys(rec)) {
494
+ const v = rec[name]
495
+ if (name === 'unwrap' || name === 'collapse' || name === 'captureTrivia' || name === 'trailingTrivia') {
496
+ if (v === true) opts[name] = true
497
+ else if (v !== false && v !== undefined) return STATIC_NODE_OPTIONS_FAILED
498
+ } else if (name === 'project') {
499
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) return STATIC_NODE_OPTIONS_FAILED
500
+ opts.project = v
501
+ } else if (name === 'tags') {
502
+ if (!Array.isArray(v) || !v.every(item => typeof item === 'string')) return STATIC_NODE_OPTIONS_FAILED
503
+ opts.tags = v
504
+ } else if (name === 'buildArity') {
505
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 0 || v > 6) return STATIC_NODE_OPTIONS_FAILED
506
+ opts.buildArity = v
507
+ }
508
+ }
509
+ return opts.unwrap || opts.collapse || opts.project !== undefined || opts.captureTrivia || opts.trailingTrivia || opts.tags !== undefined || opts.buildArity !== undefined ? opts : undefined
510
+ }
511
+
512
+ function staticNodeOptions(expr: Expression, scope: XScope): StaticNodeOptions {
513
+ const unwrapped = unwrapStaticExpr(expr)
514
+ const scoped = scopedStaticValue(unwrapped, scope)
515
+ if (scoped.found) return staticNodeOptionsFromValue(scoped.value)
516
+ if (unwrapped.type !== 'ObjectExpression') return STATIC_NODE_OPTIONS_NOT_OPTIONS
517
+ const opts: parseman.NodeOptions<readonly string[]> = {}
518
+ for (const prop of (unwrapped as ObjectExpression).properties) {
519
+ if ((prop as { type?: string }).type !== 'Property') return STATIC_NODE_OPTIONS_FAILED
520
+ const p = prop as unknown as ObjectProperty
521
+ if (p.computed) return STATIC_NODE_OPTIONS_FAILED
522
+ const name = propName(p as never)
523
+ if (name === 'unwrap' || name === 'collapse' || name === 'captureTrivia' || name === 'trailingTrivia') {
524
+ const value = staticLiteralValue(p.value)
525
+ if (value === true) opts[name] = true
526
+ else if (value !== false && value !== undefined) return STATIC_NODE_OPTIONS_FAILED
527
+ } else if (name === 'project') {
528
+ const project = staticNodeProject(p.value as Expression)
529
+ if (project?.ok === false) return STATIC_NODE_OPTIONS_FAILED
530
+ if (project !== undefined) opts.project = project.value
531
+ } else if (name === 'tags') {
532
+ const tags = staticStringArray(p.value, scope)
533
+ if (tags === undefined) return STATIC_NODE_OPTIONS_FAILED
534
+ opts.tags = tags
535
+ } else if (name === 'buildArity') {
536
+ const arity = staticLiteralValue(p.value)
537
+ if (typeof arity !== 'number' || !Number.isInteger(arity) || arity < 0 || arity > 6) return STATIC_NODE_OPTIONS_FAILED
538
+ opts.buildArity = arity
539
+ }
540
+ }
541
+ return opts.unwrap || opts.collapse || opts.project !== undefined || opts.captureTrivia || opts.trailingTrivia || opts.tags !== undefined || opts.buildArity !== undefined ? opts : undefined
542
+ }
543
+
544
+ /**
545
+ * Evaluate a call expression to a Combinator.
546
+ * `mfs` accumulates mapFn source texts in depth-first order — must match
547
+ * what codegen pushes to ctx.mapFns when it traverses the same tree.
548
+ */
549
+ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: string[]): Combinator<unknown> | null {
550
+ if (node.type === 'Identifier') return scopeGet(scope, node.name, mfs)
551
+
552
+ if (node.type !== 'CallExpression') return null
553
+
554
+ const callee = node.callee
555
+
556
+ // makeWord(boundary?, opts?)(str)
557
+ if (callee.type === 'CallExpression'
558
+ && callee.callee.type === 'Identifier'
559
+ && callee.callee.name === 'makeWord') {
560
+ const strArg = node.arguments[0]
561
+ if (!strArg || strArg.type === 'SpreadElement') return null
562
+ const factory = wordFactoryFromArgs(callee.arguments, scope, code, mfs)
563
+ const str = anyValue(strArg as Expression, scope, code, mfs)
564
+ if (factory === null || typeof str !== 'string') return null
565
+ try { return parseman.word(str, factory.boundary, { caseInsensitive: factory.caseInsensitive }) } catch { return null }
566
+ }
567
+
568
+ if (callee.type === 'Identifier') {
569
+ const factory = scope.get(callee.name)
570
+ if (isWordFactory(factory)) {
571
+ const [strArg] = node.arguments
572
+ if (!strArg || strArg.type === 'SpreadElement') return null
573
+ const str = anyValue(strArg as Expression, scope, code, mfs)
574
+ if (typeof str !== 'string') return null
575
+ try { return parseman.word(str, factory.boundary, { caseInsensitive: factory.caseInsensitive }) } catch { return null }
576
+ }
577
+ }
578
+
579
+ if (callee.type !== 'Identifier') return null
580
+
581
+ // transform(inner, fn) — capture fn source text before pushing to mfs
582
+ if (callee.name === 'transform' && code !== undefined && mfs !== undefined) {
583
+ const [parserArg, fnArg] = node.arguments
584
+ if (!parserArg || !fnArg || parserArg.type === 'SpreadElement' || fnArg.type === 'SpreadElement') return null
585
+ const inner = anyValue(parserArg as Expression, scope, code, mfs)
586
+ if (!isCombinator(inner)) return null
587
+ const fnSrc = stripTsFromSource(fnArg as Node, code)
588
+ mfs.push(fnSrc)
589
+ try {
590
+ const combi = parseman.transform(inner, (v: unknown) => v)
591
+ // Carry the callback source on the def so codegen can pull it in traversal
592
+ // order (order-independent across rules that share sub-combinators).
593
+ if (combi._def.tag === 'transform') combi._def.fnSrc = fnSrc
594
+ return combi
595
+ } catch { return null }
596
+ }
597
+
598
+ // leaf(inner, fn) — like transform(), but suppresses inner CST captures and
599
+ // publishes one reducer-selected terminal leaf to its parent.
600
+ if (callee.name === 'leaf' && code !== undefined && mfs !== undefined) {
601
+ const [parserArg, fnArg] = node.arguments
602
+ if (!parserArg || !fnArg || parserArg.type === 'SpreadElement' || fnArg.type === 'SpreadElement') return null
603
+ const inner = anyValue(parserArg as Expression, scope, code, mfs)
604
+ if (!isCombinator(inner)) return null
605
+ const fnSrc = stripTsFromSource(fnArg as Node, code)
606
+ mfs.push(fnSrc)
607
+ try {
608
+ const combi = parseman.leaf(inner, (v: unknown) => v)
609
+ if (combi._def.tag === 'leaf') combi._def.fnSrc = fnSrc
610
+ return combi
611
+ } catch { return null }
612
+ }
613
+
614
+ // node(parser, build?, opts?) / node(type, parser, build?, opts?) — CST node rule.
615
+ // Capture the build callback source (like transform) so codegen inlines it; the
616
+ // inner parser carries the capture. Options may be the trailing argument or the
617
+ // third argument when no build callback is present.
618
+ if (callee.name === 'node' && code !== undefined) {
619
+ const [firstArg, secondArg, thirdArg, fourthArg] = node.arguments
620
+ if (!firstArg || firstArg.type === 'SpreadElement') return null
621
+ const firstVal = anyValue(firstArg as Expression, scope, code, mfs)
622
+ const explicitType = typeof firstVal === 'string' ? firstVal : undefined
623
+ const parserArg = explicitType !== undefined ? secondArg : firstArg
624
+ const buildArg = explicitType !== undefined ? thirdArg : secondArg
625
+ const optsArg = explicitType !== undefined ? fourthArg : thirdArg
626
+ if (!parserArg || parserArg.type === 'SpreadElement') return null
627
+ const inner = anyValue(parserArg as Expression, scope, code, mfs)
628
+ if (!isCombinator(inner)) return null
629
+ // `build` is OPTIONAL — a structural node() omits it (or passes the literal
630
+ // `undefined` to reach the 4th opts arg). Structural nodes build via the
631
+ // injected `ctx.build` host; codegen keys that off `def.build === undefined`.
632
+ const be = buildArg as { type: string; start: number; end: number; name?: string } | undefined
633
+ const buildExpr = be === undefined || be.type === 'SpreadElement' ? undefined : unwrapStaticExpr(be as unknown as Expression)
634
+ const buildArgOptions = be !== undefined && be.type !== 'SpreadElement'
635
+ ? staticNodeOptions(buildArg as Expression, scope)
636
+ : STATIC_NODE_OPTIONS_NOT_OPTIONS
637
+ if (buildArgOptions === STATIC_NODE_OPTIONS_FAILED) return null
638
+ const buildArgIsOptions = buildArgOptions !== STATIC_NODE_OPTIONS_NOT_OPTIONS
639
+ const scopedBuild = buildExpr === undefined
640
+ ? { found: false } as const
641
+ : scopedStaticValue(buildExpr, scope)
642
+ const absentBuild = (buildExpr?.type === 'Identifier' && buildExpr.name === 'undefined')
643
+ || (buildExpr !== undefined && staticLiteralValue(buildExpr) === null)
644
+ || (scopedBuild.found && (scopedBuild.value === undefined || scopedBuild.value === null))
645
+ const hasBuild = be !== undefined && be.type !== 'SpreadElement'
646
+ && !buildArgIsOptions
647
+ && !absentBuild
648
+ const buildSrc = hasBuild ? stripTsFromSource(be! as Node, code) : undefined
649
+ let opts: parseman.NodeOptions<readonly string[]> | undefined
650
+ if (buildArgIsOptions) {
651
+ opts = buildArgOptions as parseman.NodeOptions<readonly string[]> | undefined
652
+ } else if (optsArg !== undefined) {
653
+ if (optsArg.type === 'SpreadElement') return null
654
+ const optsResult = staticNodeOptions(optsArg as Expression, scope)
655
+ if (optsResult === STATIC_NODE_OPTIONS_FAILED || optsResult === STATIC_NODE_OPTIONS_NOT_OPTIONS) return null
656
+ opts = optsResult
657
+ }
658
+ try {
659
+ const combi = explicitType !== undefined
660
+ ? parseman.node(explicitType, inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
661
+ : parseman.node(inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
662
+ if (combi._def.tag === 'node' && buildSrc !== undefined) {
663
+ combi._def.buildSrc = buildSrc
664
+ // The type argument's IDENTIFIER, when it was written as one. A `node(type, …)`
665
+ // inside a factory resolves `type` to a string here, which loses the fact that
666
+ // the reducer's `mk(type, …)` names the SAME binding — and losing it is what
667
+ // made every factory-built node miss the inline-`mk` path.
668
+ if (explicitType !== undefined && firstArg.type === 'Identifier') {
669
+ combi._def.typeSrc = (firstArg as unknown as { name: string }).name
670
+ }
671
+ // A NAMED reducer (`foldOperation`, `helpers.fold`, an import): resolve it so the
672
+ // capture-tier analysis reads the REAL parameter list instead of failing open.
673
+ // `null` means the expression was an inline function, which is self-describing.
674
+ const resolved = _reducers?.resolve(buildSrc, be!.start, code)
675
+ if (resolved) {
676
+ if (resolved.src !== null) combi._def.buildSigSrc = resolved.src
677
+ /*
678
+ * A reducer named from a FOREIGN factory has to be emitted as its SOURCE, not
679
+ * its name. `buildSrc` is the call site's expression text, and the call site is
680
+ * in the factory's module — so `node('Fold', …, fold)` in an imported factory
681
+ * emitted `const _build = [fold]` into the CONSUMING module, where `fold` is a
682
+ * module-private const of a file that was never imported. That artifact threw
683
+ * `ReferenceError: fold is not defined` on import, and nothing noticed: the
684
+ * shape has tests, but they assert on the emitted TEXT and never run it. The
685
+ * emit-time scope check below `ms.toString()` is what found it.
686
+ *
687
+ * The resolver has already read the declaration out of the right module, so the
688
+ * substitution is exact for a self-contained reducer. One that closes over more
689
+ * of its own module's privates is NOT fixed by this — it is caught by that same
690
+ * scope check, which refuses to emit rather than shipping the next ReferenceError.
691
+ */
692
+ if (_entrySource !== null && code !== _entrySource
693
+ && resolved.src !== null && be!.type === 'Identifier') {
694
+ combi._def.buildSrc = resolved.src
695
+ }
696
+ // An author-declared `node(..., { buildArity })` is authority 1 in
697
+ // `confirmedArityForDef`; the resolver is authority 2. Both land in the SAME
698
+ // field, so writing unconditionally here demoted the declaration to whatever
699
+ // scope analysis happened to find.
700
+ if (combi._def.buildArity === undefined && resolved.arity !== null) combi._def.buildArity = resolved.arity
701
+ if (resolved.reason !== undefined) combi._def.buildArityUnresolved = resolved.reason
702
+ }
703
+ const staticError = directBuilderUnsupportedBindings(buildSrc)
704
+ if (staticError.length > 0) combi._def.buildStaticError = staticError
705
+ }
706
+ return combi
707
+ } catch { return null }
708
+ }
709
+
710
+ // rules(factory) — handled separately by evaluateParserFactory; signal null here
711
+ if (callee.name === 'rules') return null
712
+
713
+ // ref() — forward-declared recursion slot. Standalone refs (declared, then
714
+ // resolved later via `x.define(...)`) are the interpreter/compile() recursion
715
+ // mechanism; the macro must support them too for parity. We return a REAL ref
716
+ // placeholder here; index.ts pre-resolves all `x.define(...)` statements into
717
+ // scope before compilation so codegen's emitLazy sees a defined thunk.
718
+ if (callee.name === 'ref') {
719
+ if (node.arguments.length !== 0) return null
720
+ return ref<unknown>() as Combinator<unknown>
721
+ }
722
+
723
+ // parser(opts, root) — bakes trivia/trackLines into a `grammar` combinator so
724
+ // the compiled output skips whitespace between sequence terms identically to
725
+ // the interpreter. opts.trivia is itself a combinator; evaluate it with a
726
+ // throwaway mfs accumulator since the trivia parser is emitted out-of-band
727
+ // (ensureTriviaFn) and its sources are pulled via def.fnSrc, not positionally.
728
+ if (callee.name === 'parser') {
729
+ const [optsArg, rootArg] = node.arguments
730
+ if (!optsArg || !rootArg || optsArg.type === 'SpreadElement' || rootArg.type === 'SpreadElement') return null
731
+ const opts = anyValue(optsArg as Expression, scope, code, [])
732
+ if (!opts || typeof opts !== 'object') return null
733
+ const root = anyValue(rootArg as Expression, scope, code, mfs)
734
+ if (!isCombinator(root)) return null
735
+ try {
736
+ return parseman.parser(opts as parseman.ParserOptions, root)
737
+ } catch { return null }
738
+ }
739
+
740
+ // sepBy(item, sep, opts?) — emitSepBy traverses: item (first probe), sep, item
741
+ // (loop body). We must push item's mfSrcs twice to stay aligned with ctx.mapFns.
742
+ // `opts` (notably `{ min: 1 }`) MUST be honored: dropping it would silently
743
+ // compile a NULLABLE list where the source asked for a non-empty one.
744
+ if (callee.name === 'sepBy' || callee.name === 'oneOrMoreSep') {
745
+ const [itemArg, sepArg, optsArg] = node.arguments
746
+ if (!itemArg || !sepArg || itemArg.type === 'SpreadElement' || sepArg.type === 'SpreadElement') return null
747
+ const itemMfs: string[] = []
748
+ const itemCombi = anyValue(itemArg as Expression, scope, code, itemMfs)
749
+ if (!isCombinator(itemCombi)) return null
750
+ const sepMfs: string[] = []
751
+ const sepCombi = anyValue(sepArg as Expression, scope, code, sepMfs)
752
+ if (!isCombinator(sepCombi)) return null
753
+ let opts: parseman.SepByOptions | undefined
754
+ if (optsArg) {
755
+ if (optsArg.type === 'SpreadElement') return null
756
+ const v = anyValue(optsArg as Expression, scope, code, [])
757
+ if (!v || typeof v !== 'object') return null
758
+ opts = v as parseman.SepByOptions
759
+ }
760
+ if (mfs) mfs.push(...itemMfs, ...sepMfs, ...itemMfs)
761
+ try {
762
+ return callee.name === 'sepBy'
763
+ ? parseman.sepBy(itemCombi, sepCombi, opts)
764
+ : parseman.oneOrMoreSep(itemCombi, sepCombi, opts)
765
+ } catch { return null }
766
+ }
767
+
768
+ // many(item, opts?) / oneOrMore(item, opts?) — emitMany traverses `min` mandatory
769
+ // items then the loop body, so item's mfSrcs are replayed min+1 times. `opts`
770
+ // MUST be honored: dropping `{ min }` would silently compile a NULLABLE repeat.
771
+ if (callee.name === 'oneOrMore' || callee.name === 'many') {
772
+ const [itemArg, optsArg] = node.arguments
773
+ if (!itemArg || itemArg.type === 'SpreadElement') return null
774
+ const itemMfs: string[] = []
775
+ const itemCombi = anyValue(itemArg as Expression, scope, code, itemMfs)
776
+ if (!isCombinator(itemCombi)) return null
777
+ let opts: parseman.RepeatOptions | undefined
778
+ if (optsArg) {
779
+ if (optsArg.type === 'SpreadElement') return null
780
+ const v = anyValue(optsArg as Expression, scope, code, [])
781
+ if (!v || typeof v !== 'object') return null
782
+ opts = v as parseman.RepeatOptions
783
+ }
784
+ let combi: Combinator<unknown>
785
+ try {
786
+ combi = callee.name === 'many' ? parseman.many(itemCombi, opts) : parseman.oneOrMore(itemCombi, opts)
787
+ } catch { return null }
788
+ const min = combi._def.tag === 'oneOrMore' ? combi._def.min : 0
789
+ if (mfs) for (let i = 0; i <= min; i++) mfs.push(...itemMfs)
790
+ return combi
791
+ }
792
+
793
+ // not(parser) — negative lookahead (consumes nothing).
794
+ if (callee.name === 'not') {
795
+ const [innerArg] = node.arguments
796
+ if (!innerArg || innerArg.type === 'SpreadElement') return null
797
+ const inner = anyValue(innerArg as Expression, scope, code, mfs)
798
+ if (!isCombinator(inner)) return null
799
+ try { return parseman.not(inner) } catch { return null }
800
+ }
801
+
802
+ // peek(parser) — POSITIVE lookahead (consumes nothing), carrying the body's
803
+ // first-set so a leading peek() still gates its choice arm.
804
+ if (callee.name === 'peek') {
805
+ const [innerArg] = node.arguments
806
+ if (!innerArg || innerArg.type === 'SpreadElement') return null
807
+ const inner = anyValue(innerArg as Expression, scope, code, mfs)
808
+ if (!isCombinator(inner)) return null
809
+ try { return parseman.peek(inner) } catch { return null }
810
+ }
811
+
812
+ // balanced(open, close, opts?) — like scanTo, opts (notably opts.skip, an array
813
+ // of combinators) MUST be honored. The interpreter and compile() build the full
814
+ // combinator structure from opts; the macro must evaluate and pass opts too —
815
+ // dropping it silently produces wrong (parity-breaking) behavior.
816
+ if (callee.name === 'balanced') {
817
+ const [openArg, closeArg, optsArg] = node.arguments
818
+ if (!openArg || !closeArg || openArg.type === 'SpreadElement' || closeArg.type === 'SpreadElement') return null
819
+ const open = anyValue(openArg as Expression, scope, code, [])
820
+ const close = anyValue(closeArg as Expression, scope, code, [])
821
+ if (typeof open !== 'string' || typeof close !== 'string') return null
822
+ const opts = optsArg && optsArg.type !== 'SpreadElement'
823
+ ? anyValue(optsArg as Expression, scope, code, [])
824
+ : undefined
825
+ try { return parseman.balanced(open, close, opts as parseman.ScanToOptions | undefined) } catch { return null }
826
+ }
827
+
828
+ // scanTo(sentinel, opts?) — consume up to (and including) a sentinel, optionally
829
+ // skipping balanced pairs. opts.skip is an array of combinators.
830
+ if (callee.name === 'scanTo') {
831
+ const [sentinelArg, optsArg] = node.arguments
832
+ if (!sentinelArg || sentinelArg.type === 'SpreadElement') return null
833
+ const sentinel = anyValue(sentinelArg as Expression, scope, code, [])
834
+ if (!isCombinator(sentinel)) return null
835
+ const opts = optsArg && optsArg.type !== 'SpreadElement'
836
+ ? anyValue(optsArg as Expression, scope, code, [])
837
+ : undefined
838
+ try { return parseman.scanTo(sentinel, opts as parseman.ScanToOptions | undefined) } catch { return null }
839
+ }
840
+
841
+ // gate(pred) (formerly guard(pred)) — context assertion. Capture the predicate
842
+ // source (like transform's fn) so codegen inlines it into `_mf`; build a
843
+ // placeholder gate and stash the source on `_def.predSrc`. Without source-capture
844
+ // context we return null → the whole rule falls back to the (correct)
845
+ // interpreter, never dropping the predicate.
846
+ if (callee.name === 'gate' || callee.name === 'guard') {
847
+ if (code === undefined || mfs === undefined) return null
848
+ const [predArg] = node.arguments
849
+ if (!predArg || predArg.type === 'SpreadElement') return null
850
+ const predSrc = stripTsFromSource(predArg as Node, code)
851
+ mfs.push(predSrc)
852
+ try {
853
+ const combi = parseman.gate(() => true)
854
+ if (combi._def.tag !== 'guard') return null
855
+ combi._def.predSrc = predSrc
856
+ return combi
857
+ } catch { return null }
858
+ }
859
+
860
+ // withCtx(extra, inner) — run `inner` with ctx.state = extra. Capture the
861
+ // `extra` argument source; codegen wraps it as `() => (extra)` in `_mf`. The
862
+ // extra getter is emitted BEFORE the inner parser's own mapFns (matching
863
+ // emitWithCtx's push order), so push the extra token first, then eval inner.
864
+ if (callee.name === 'withCtx') {
865
+ if (code === undefined || mfs === undefined) return null
866
+ const [extraArg, innerArg] = node.arguments
867
+ if (!extraArg || !innerArg || extraArg.type === 'SpreadElement' || innerArg.type === 'SpreadElement') return null
868
+ const extraSrc = stripTsFromSource(extraArg as Node, code)
869
+ mfs.push(extraSrc)
870
+ const inner = anyValue(innerArg as Expression, scope, code, mfs)
871
+ if (!isCombinator(inner)) return null
872
+ try {
873
+ // THE VALUE, not just its source text. Codegen only ever needed `extraSrc`
874
+ // — it prints `() => (extra)` into `_mf` — so `{}` was an adequate stand-in
875
+ // for the def's own `extra`. The TABLE ENCODER reads `d.extra` and interns
876
+ // it in the const pool, so the placeholder became the artifact: the pool
877
+ // held a bare `{}` and every `withCtx` gate predicate (`s => !!(s && s.inner)`)
878
+ // was present and permanently false. Evaluate the argument; the placeholder
879
+ // survives only when it cannot be evaluated, and then it is `emittableConst`
880
+ // that decides — a plain `{}` extras object is indistinguishable from an
881
+ // author's `{}`, so an unevaluable one must NOT masquerade as empty state.
882
+ const evaluated = anyValue(extraArg as Expression, scope, code, [])
883
+ const usable = typeof evaluated === 'object' && evaluated !== null && !Array.isArray(evaluated)
884
+ && Object.getPrototypeOf(evaluated) === Object.prototype
885
+ const combi = parseman.withCtx(usable ? evaluated : UNEVALUATED_EXTRA, inner)
886
+ if (combi._def.tag !== 'withCtx') return null
887
+ combi._def.extraSrc = extraSrc
888
+ return combi
889
+ } catch { return null }
890
+ }
891
+
892
+ if (callee.name === 'dispatch') {
893
+ const [selectorArg, ...armArgs] = node.arguments
894
+ if (!selectorArg || selectorArg.type === 'SpreadElement') return null
895
+ const selector = anyValue(selectorArg as Expression, scope, code, mfs)
896
+ if (!isCombinator(selector)) return null
897
+ const arms: DispatchArm<unknown>[] = []
898
+ for (const arg of armArgs) {
899
+ if (arg.type === 'SpreadElement') return null
900
+ const arm = dispatchArmValue(arg as Expression, scope, code, mfs)
901
+ if (!isDispatchArm(arm)) return null
902
+ arms.push(arm)
903
+ }
904
+ try {
905
+ const unsafeDispatch = parseman.dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
906
+ return unsafeDispatch(selector as Combinator<string>, ...arms)
907
+ } catch { return null }
908
+ }
909
+
910
+ // choice(...) WITH at least one gated arm `{ gate, combinator }`. The generic
911
+ // SUPPORTED path evaluates a gated-arm ObjectExpression via anyValue, whose
912
+ // arrow-gate evaluates to `null` — so `choice` would treat the arm as UNGATED
913
+ // and emit it unconditionally (a SILENT semantic miscompile vs the interpreter).
914
+ // Handle gated choices explicitly: capture each gate's source, build the REAL
915
+ // gated arm, and stash the per-arm sources on `_def.gateSrcs`. If any gate can't
916
+ // be source-captured, return null for the WHOLE choice → safe interpreter
917
+ // fallback. (Non-gated choices fall through to the generic path → byte-identical.)
918
+ if (callee.name === 'choice' && node.arguments.some(a => a.type !== 'SpreadElement' && isGatedArmExpr(a as { type: string }))) {
919
+ if (code === undefined || mfs === undefined) return null
920
+ const arms: Array<Combinator<unknown> | { gate: (s: unknown) => boolean; combinator: Combinator<unknown> }> = []
921
+ const gateSrcs: (string | null)[] = []
922
+ for (const argNode of node.arguments) {
923
+ if (argNode.type === 'SpreadElement') return null
924
+ if (isGatedArmExpr(argNode as { type: string })) {
925
+ const parts = gatedArmParts(argNode as unknown as ObjectExpression)
926
+ if (!parts) return null
927
+ // Gate mapFn is pushed BEFORE the arm body's mapFns (matches emitFirstMatch).
928
+ const gateSrc = stripTsFromSource(parts.gate as unknown as Node, code)
929
+ mfs.push(gateSrc)
930
+ const combi = anyValue(parts.combinator, scope, code, mfs)
931
+ if (!isCombinator(combi)) return null
932
+ arms.push({ gate: () => true, combinator: combi })
933
+ gateSrcs.push(gateSrc)
934
+ } else {
935
+ const combi = anyValue(argNode as Expression, scope, code, mfs)
936
+ if (!isCombinator(combi)) return null
937
+ arms.push(combi)
938
+ gateSrcs.push(null)
939
+ }
940
+ }
941
+ try {
942
+ const combi = (parseman.choice as (...p: unknown[]) => Combinator<unknown>)(...arms)
943
+ if (combi._def.tag !== 'choice') return null
944
+ combi._def.gateSrcs = gateSrcs
945
+ // Guard: a real gate MUST align with a captured source, and vice-versa —
946
+ // no predicate-bearing arm may reach codegen with a dropped source.
947
+ for (let i = 0; i < combi._def.gates.length; i++) {
948
+ if ((combi._def.gates[i] !== null) !== (gateSrcs[i] !== null)) return null
949
+ }
950
+ return combi
951
+ } catch { return null }
952
+ }
953
+
954
+ const factory = SUPPORTED[callee.name]
955
+ if (!factory) return null
956
+
957
+ const args = node.arguments.map(arg => {
958
+ if (arg.type === 'SpreadElement') return null
959
+ return anyValue(arg as Expression, scope, code, mfs)
960
+ })
961
+ if (args.some(a => a === null)) return null
962
+
963
+ try {
964
+ return factory(...(args as unknown[]))
965
+ } catch { return null }
966
+ }
967
+
968
+ /** Evaluate any expression to its JS value (not necessarily a Combinator). */
969
+ function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]): unknown {
970
+ if (node.type === 'TSAsExpression'
971
+ || node.type === 'TSSatisfiesExpression'
972
+ || node.type === 'TSNonNullExpression'
973
+ || node.type === 'TSTypeAssertion'
974
+ || node.type === 'TSInstantiationExpression'
975
+ || node.type === 'ParenthesizedExpression') {
976
+ const inner = (node as unknown as { expression?: Expression }).expression
977
+ return inner ? anyValue(inner, scope, code, mfs) : null
978
+ }
979
+
980
+ if (node.type === 'Literal') {
981
+ if ('regex' in node && node.regex !== null && node.regex !== undefined) {
982
+ return new RegExp(node.regex.pattern, node.regex.flags)
983
+ }
984
+ return node.value
985
+ }
986
+
987
+ if (node.type === 'ArrayExpression') {
988
+ const arr = node as unknown as { elements: Array<Expression | null> }
989
+ const out: unknown[] = []
990
+ for (const el of arr.elements) {
991
+ if (el === null) { out.push(null); continue }
992
+ if ((el as { type: string }).type === 'SpreadElement') return null
993
+ out.push(anyValue(el as Expression, scope, code, mfs))
994
+ }
995
+ return out
996
+ }
997
+
998
+ if (node.type === 'ObjectExpression') {
999
+ const obj: Record<string, unknown> = {}
1000
+ for (const prop of node.properties) {
1001
+ const key = propName(prop as never)
1002
+ if (key === null) return null
1003
+ obj[key] = anyValue((prop as unknown as ObjectProperty).value as Expression, scope, code, mfs)
1004
+ }
1005
+ return obj
1006
+ }
1007
+
1008
+ if (node.type === 'Identifier') {
1009
+ if (node.name === 'undefined') return undefined
1010
+ const entry = scope.get(node.name) ?? null
1011
+ if (isScopeEntry(entry)) {
1012
+ if (mfs && entry.mfSrcs.length > 0) mfs.push(...entry.mfSrcs)
1013
+ return entry.combi
1014
+ }
1015
+ if (isStaticValueEntry(entry)) {
1016
+ if (mfs && entry.mfSrcs.length > 0) mfs.push(...entry.mfSrcs)
1017
+ return entry.value
1018
+ }
1019
+ return entry
1020
+ }
1021
+
1022
+ // MemberExpression — handles g.ruleName references inside parser() factories
1023
+ if (node.type === 'MemberExpression') {
1024
+ const mem = node as unknown as StaticMemberExpression
1025
+ const obj = anyValue(mem.object as Expression, scope, code, mfs)
1026
+ if (!obj || typeof obj !== 'object') return null
1027
+ if ((node as unknown as { computed: boolean }).computed) {
1028
+ const key = anyValue((node as unknown as { property: Expression }).property, scope, code, mfs)
1029
+ if (typeof key !== 'string' && typeof key !== 'number') return null
1030
+ return (obj as Record<string | number, unknown>)[key] ?? null
1031
+ }
1032
+ // A member ACCESS name (`obj.foo`), not an object-literal key — a different thing
1033
+ // from `propName`, and named apart from it so it cannot shadow the shared reader.
1034
+ const memberName = (mem.property as { name?: string }).name
1035
+ if (!memberName) return null
1036
+ return (obj as Record<string, unknown>)[memberName] ?? null
1037
+ }
1038
+
1039
+ if (node.type === 'CallExpression') {
1040
+ const callee = node.callee
1041
+ if (callee.type === 'Identifier' && callee.name === 'makeWord') {
1042
+ return wordFactoryFromArgs(node.arguments, scope, code, mfs)
1043
+ }
1044
+ if (callee.type === 'Identifier' && callee.name === 'makeWhen') {
1045
+ return whenFactoryFromArgs(node.arguments, scope, code, mfs)
1046
+ }
1047
+ if (callee.type === 'Identifier' && (callee.name === 'startsWith' || callee.name === 'endsWith')) {
1048
+ if (node.arguments.length !== 1 || node.arguments[0]?.type === 'SpreadElement') return null
1049
+ const value = anyValue(node.arguments[0] as Expression, scope, code, mfs)
1050
+ if (typeof value !== 'string') return null
1051
+ return callee.name === 'startsWith' ? parseman.startsWith(value) : parseman.endsWith(value)
1052
+ }
1053
+ if (callee.type === 'Identifier' && callee.name === 'matches') {
1054
+ if (node.arguments.length !== 1 || node.arguments[0]?.type === 'SpreadElement') return null
1055
+ const value = anyValue(node.arguments[0] as Expression, scope, code, mfs)
1056
+ if (!(value instanceof RegExp)) return null
1057
+ return parseman.matches(value)
1058
+ }
1059
+ if (callee.type === 'Identifier' && (callee.name === 'when' || callee.name === 'otherwise')) {
1060
+ return dispatchArmValue(node, scope, code, mfs)
1061
+ }
1062
+ if (callee.type === 'Identifier' && isWhenFactory(scope.get(callee.name))) {
1063
+ return dispatchArmValue(node, scope, code, mfs)
1064
+ }
1065
+ return exprToCombi(node, scope, code, mfs)
1066
+ }
1067
+
1068
+ return null
1069
+ }
1070
+
1071
+ // ---------------------------------------------------------------------------
1072
+ // Public API
1073
+ // ---------------------------------------------------------------------------
1074
+
1075
+ /** Evaluate makeWord(boundary?, opts?) to a factory entry (not a combinator). */
1076
+ export function evaluateWordFactory(
1077
+ node: Expression,
1078
+ scope: Scope,
1079
+ code?: string,
1080
+ ): WordFactoryEntry | null {
1081
+ if (node.type !== 'CallExpression') return null
1082
+ const callee = node.callee
1083
+ if (callee.type !== 'Identifier' || callee.name !== 'makeWord') return null
1084
+ return wordFactoryFromArgs(node.arguments, scope as XScope, code)
1085
+ }
1086
+
1087
+ /** Evaluate makeWhen(opts?) to a dispatch-arm factory entry (not a combinator). */
1088
+ export function evaluateWhenFactory(
1089
+ node: Expression,
1090
+ scope: Scope,
1091
+ code?: string,
1092
+ ): WhenFactoryEntry | null {
1093
+ if (node.type !== 'CallExpression') return null
1094
+ const callee = node.callee
1095
+ if (callee.type !== 'Identifier' || callee.name !== 'makeWhen') return null
1096
+ return whenFactoryFromArgs(node.arguments, scope as XScope, code)
1097
+ }
1098
+
1099
+ /** Evaluate a single combinator expression. Returns null if unresolvable. */
1100
+ export function evaluateExpr(
1101
+ node: Expression,
1102
+ scope: Scope,
1103
+ code?: string,
1104
+ mapFnSources?: string[],
1105
+ ): Combinator<unknown> | null {
1106
+ return exprToCombi(node, scope as XScope, code, mapFnSources)
1107
+ }
1108
+
1109
+ /**
1110
+ * Evaluate a `const X = [combinator, …]` array literal into an array of
1111
+ * Combinators. Lets a shared option array (e.g. a `skip` set reused across
1112
+ * `scanTo`/`balanced` calls) be referenced by name — `{ skip: X }` — instead of
1113
+ * inlining the array at every call site. Returns null when `node` isn't an array
1114
+ * literal of statically-resolvable combinators.
1115
+ */
1116
+ export function evaluateCombinatorArray(
1117
+ node: Expression,
1118
+ scope: Scope,
1119
+ code?: string,
1120
+ ): Combinator<unknown>[] | null {
1121
+ if (node.type !== 'ArrayExpression') return null
1122
+ const val = anyValue(node, scope as XScope, code, [])
1123
+ if (!Array.isArray(val) || val.length === 0) return null
1124
+ if (!val.every(isCombinator)) return null
1125
+ return val as Combinator<unknown>[]
1126
+ }
1127
+
1128
+ // ---------------------------------------------------------------------------
1129
+ // A rules() factory's returned object is a flat map of `key: combinator` — the
1130
+ // ONLY composition mechanism is compose() (see linker.ts). `...frag(g)` spreads
1131
+ // are not supported: a spread property makes the factory non-statically-evaluable
1132
+ // (propName returns null below), so it falls back to the interpreter.
1133
+ // ---------------------------------------------------------------------------
1134
+
1135
+ /** Collect every rule key from a rules() return object. A non-`key: value`
1136
+ * property (spread / computed / rest) → null → the caller falls back. */
1137
+ function collectRuleKeys(retObj: ObjectExpression): string[] | null {
1138
+ const out: string[] = []
1139
+ for (const prop of (retObj as unknown as { properties: Array<{ type: string }> }).properties) {
1140
+ const key = propName(prop as never)
1141
+ if (!key) return null
1142
+ out.push(key)
1143
+ }
1144
+ return out
1145
+ }
1146
+
1147
+ export function evaluateStaticValue(
1148
+ node: Expression,
1149
+ scope: Scope,
1150
+ code?: string,
1151
+ ): unknown {
1152
+ return anyValue(node, scope as XScope, code, [])
1153
+ }
1154
+
1155
+ type RuleEntry = { key: string; value: Expression; scope: XScope; code: string }
1156
+
1157
+ /** Flatten a rules() return object into ordered (key, valueExpr, evalScope).
1158
+ * A non-`key: value` property → null → interpreter fallback. */
1159
+ function flattenRuleEntries(retObj: ObjectExpression, scope: XScope, code: string): RuleEntry[] | null {
1160
+ const out: RuleEntry[] = []
1161
+ for (const prop of (retObj as unknown as { properties: Array<{ type: string; value?: unknown }> }).properties) {
1162
+ const key = propName(prop as never)
1163
+ if (!key) return null
1164
+ out.push({ key, value: (prop as { value: Expression }).value, scope, code })
1165
+ }
1166
+ return out
1167
+ }
1168
+
1169
+ /** Every `Identifier` name appearing anywhere under an expression node. */
1170
+ function identifierNamesIn(node: unknown, out: Set<string> = new Set()): Set<string> {
1171
+ if (!node || typeof node !== 'object') return out
1172
+ if (Array.isArray(node)) { for (const n of node) identifierNamesIn(n, out); return out }
1173
+ const rec = node as Record<string, unknown>
1174
+ if (rec.type === 'Identifier' && typeof rec.name === 'string') out.add(rec.name)
1175
+ for (const k of Object.keys(rec)) {
1176
+ if (k === 'type' || k === 'start' || k === 'end') continue
1177
+ identifierNamesIn(rec[k], out)
1178
+ }
1179
+ return out
1180
+ }
1181
+
1182
+ /**
1183
+ * Evaluate a factory body's `const` declarations into `scope`. Returns false on failure,
1184
+ * and writes a specific reason into `out.reason`.
1185
+ *
1186
+ * The reason matters more than it looks. A body binding that fails takes the whole
1187
+ * factory down, and the caller's generic "isn't statically evaluable" — or, through
1188
+ * `composeLeaf`, a message about the ARGUMENT SHAPE — points nowhere near the cause.
1189
+ * The dominant real cause is a forward reference: `const A = node('A', B, …)` above
1190
+ * `const B = …`. That is a JavaScript temporal dead zone, not a macro limitation — the
1191
+ * interpreter throws `ReferenceError: Cannot access 'B' before initialization` on the
1192
+ * very same source — so the macro should say so as plainly as the interpreter does.
1193
+ * `g.B` is order-free (the proxy mints a ref and defines it in phase 2), which is why
1194
+ * converting a `g.` reference to a bare const can only move DOWN the file.
1195
+ */
1196
+ function evalBodyStatements(
1197
+ statements: VariableDeclaration[],
1198
+ scope: XScope,
1199
+ code: string,
1200
+ out?: { reason?: string },
1201
+ ): boolean {
1202
+ const bodyMfs: string[] = []
1203
+ // Names bound later in this body, for forward-reference attribution.
1204
+ const laterNames: Array<Set<string>> = []
1205
+ {
1206
+ const all: string[][] = statements.map(stmt =>
1207
+ stmt.declarations.map(d => ((d as unknown as VariableDeclarator).id as unknown as { name?: string }).name ?? '')
1208
+ )
1209
+ const flat = all.flat()
1210
+ let seen = 0
1211
+ for (const names of all) {
1212
+ laterNames.push(new Set(flat.slice(seen + names.length)))
1213
+ seen += names.length
1214
+ }
1215
+ }
1216
+ let si = -1
1217
+ for (const stmt of statements) {
1218
+ si++
1219
+ for (const d of stmt.declarations) {
1220
+ const decl = d as unknown as VariableDeclarator
1221
+ const id = decl.id as unknown as { type: string; name?: string }
1222
+ const name = id.name ?? '<destructured>'
1223
+ if (!decl.init) { if (out) out.reason = `\`${name}\` has no initializer`; return false }
1224
+ if (id.type !== 'Identifier' && id.type !== 'BindingIdentifier') {
1225
+ if (out) out.reason = 'a destructuring binding in the factory body'
1226
+ return false
1227
+ }
1228
+ const before = bodyMfs.length
1229
+ const val = anyValue(decl.init as unknown as Expression, scope, code, bodyMfs)
1230
+ if (val === null) {
1231
+ if (out) {
1232
+ const used = identifierNamesIn(decl.init)
1233
+ const forward = [...(laterNames[si] ?? [])].filter(n => n !== '' && used.has(n))
1234
+ out.reason = forward.length > 0
1235
+ ? `\`${name}\` references ${forward.map(n => `\`${n}\``).join(', ')} before ${forward.length > 1 ? 'their declarations' : 'its declaration'}`
1236
+ + ` — a temporal dead zone (the interpreter throws "Cannot access '${forward[0]}' before initialization" on this source too);`
1237
+ + ` move the declaration above \`${name}\`, or use \`g.${forward[0]}\`, which is order-free`
1238
+ : `\`${name}\` isn't a statically-evaluable combinator`
1239
+ }
1240
+ return false
1241
+ }
1242
+ const thisDeclMfSrcs = bodyMfs.slice(before)
1243
+ if (isCombinator(val)) scope.set(name, { combi: val, mfSrcs: thisDeclMfSrcs } satisfies ScopeEntry)
1244
+ else if (isDispatchArm(val)) scope.set(name, { value: val, mfSrcs: thisDeclMfSrcs } satisfies StaticValueEntry)
1245
+ else scope.set(name, val)
1246
+ }
1247
+ }
1248
+ return true
1249
+ }
1250
+
1251
+ /**
1252
+ * Evaluate a `parser(g => { ... return { ruleName: combinator, ... } })` call.
1253
+ * Returns a map of rule names → defined Combinators, or null if the factory
1254
+ * can't be statically evaluated.
1255
+ *
1256
+ * mapFnSources is populated with the sources for mapFns that codegen will push
1257
+ * when compiling each returned rule — each rule's entry in the map will produce
1258
+ * a sub-slice of mapFnSources aligned to its specific ctx.mapFns.
1259
+ *
1260
+ * Important: this function uses a SEPARATE accumulator for body statement
1261
+ * evaluation so that only the return-expression phase adds entries to the
1262
+ * caller-provided mapFnSources (which is what compile() will receive).
1263
+ * The body-phase entries are stored as `mfSrcs` on localScope entries and
1264
+ * replayed when those entries are referenced during return evaluation.
1265
+ */
1266
+ export function evaluateParserFactory(
1267
+ factoryNode: Expression,
1268
+ scope: Scope,
1269
+ code: string,
1270
+ mapFnSources: string[], // receives ONLY the return-expression mfSrcs
1271
+ out?: { reason?: string }, // receives a SPECIFIC failure reason (see evalBodyStatements)
1272
+ /**
1273
+ * The `rules({ … }, factory)` options this call site declared, THREADED THROUGH
1274
+ * rather than reapplied afterwards.
1275
+ *
1276
+ * `plugin/index.ts` used to stamp `grammarScanSkip`, `grammarHostMode` and
1277
+ * `grammarTrackLines` onto the evaluated rules in three loops of its own,
1278
+ * carrying a comment that it had to "because the macro evaluates the FACTORY
1279
+ * directly and never calls `rules()`". It calls `rules()` now, so the options
1280
+ * belong where every other caller puts them: in the argument. That also gets
1281
+ * the `trackLines` half right for the first time — `rules()` does not merely
1282
+ * stamp it, it WRAPS each rule in a `grammarParser({ trackLines: true })` scope
1283
+ * (`parser.ts:228-242`), which the macro's stamp-only copy never did.
1284
+ */
1285
+ options?: RulesOptions,
1286
+ ): Map<string, Combinator<unknown>> | null {
1287
+ if (factoryNode.type !== 'ArrowFunctionExpression' && factoryNode.type !== 'FunctionDeclaration' && factoryNode.type !== 'FunctionExpression') return null
1288
+
1289
+ const factory = factoryNode as unknown as ArrowFunctionExpression | OxcFunction
1290
+ const params = factory.params
1291
+ if (params.length !== 1) return null
1292
+ const param = params[0] as unknown as { type: string; name?: string }
1293
+ // FormalParameter is { decorators? } & BindingPattern — BindingIdentifier has type "Identifier"
1294
+ const proxyName = param.type === 'Identifier' ? param.name ?? null : null
1295
+ if (!proxyName) return null
1296
+
1297
+ const body = factory.body
1298
+ if (!body) return null
1299
+ const statements: VariableDeclaration[] = []
1300
+ let returnExpr: Expression | null = null
1301
+
1302
+ if ((body as unknown as { type: string }).type === 'BlockStatement') {
1303
+ const stmts = (body as unknown as { body: unknown[] }).body
1304
+ for (const stmt of stmts) {
1305
+ const s = stmt as { type: string }
1306
+ if (s.type === 'ReturnStatement') {
1307
+ returnExpr = ((s as unknown as ReturnStatement).argument ?? null) as Expression | null
1308
+ break
1309
+ }
1310
+ if (s.type === 'VariableDeclaration') {
1311
+ statements.push(s as unknown as VariableDeclaration)
1312
+ } else {
1313
+ return null // unsupported statement type
1314
+ }
1315
+ }
1316
+ } else {
1317
+ // Concise arrow body: g => ({ ... })
1318
+ returnExpr = body as unknown as Expression
1319
+ }
1320
+
1321
+ if (!returnExpr) return null
1322
+
1323
+ // Unwrap parenthesized expression if needed
1324
+ const retObj = returnExpr.type === 'ParenthesizedExpression'
1325
+ ? (returnExpr as unknown as { expression: Expression }).expression
1326
+ : returnExpr
1327
+ if (retObj.type !== 'ObjectExpression') return null
1328
+
1329
+ // Pre-scan the return object for rule names. A non-`key: value` property (spread,
1330
+ // computed, rest) → null → the caller falls back to the interpreter. One ref per
1331
+ // UNIQUE key (first occurrence).
1332
+ const keys = collectRuleKeys(retObj as unknown as ObjectExpression)
1333
+ if (!keys) return null
1334
+
1335
+ /*
1336
+ * ── ONE GRAMMAR-EVALUATION PATH ────────────────────────────────────────────
1337
+ *
1338
+ * Everything below this point used to be a SECOND IMPLEMENTATION of `rules()`:
1339
+ * mint a `ref()` per key, build a `g` proxy that hands back a placeholder for
1340
+ * any name, evaluate, define each slot, tag each rule. `rules()`
1341
+ * (`combinators/parser.ts:136`) does exactly that, and the two had drifted —
1342
+ * the copy never ran the closing `markUnusedValues`, so every macro-lowered
1343
+ * grammar reached the encoder with `valueUnused` unset and the shipped artifact
1344
+ * built 318 sequence tuples and 90 repeat arrays per parse of `benchmark.less`
1345
+ * that nothing reads. That was fixed by calling the real pass; this removes the
1346
+ * copy that made the omission possible, so there is nothing left to omit.
1347
+ *
1348
+ * `rules()` takes a FACTORY, which is precisely what this function has — not as
1349
+ * a JS closure, but as an AST it can evaluate on demand. So the collapse is to
1350
+ * hand `rules()` a closure that evaluates that AST against whatever proxy
1351
+ * `rules()` supplies, and let `rules()` own every step it already owned:
1352
+ *
1353
+ * - the `g` proxy, INCLUDING the external-ref behaviour. `rules()`'s proxy
1354
+ * mints a tagged placeholder for ANY name touched, which is what the local
1355
+ * `externalRefs` map was reproducing — a `g.X` this grammar references but
1356
+ * does not define, bound later by the fuse.
1357
+ * - the define loop, the self-alias check, `tagRule` (byte-identical to the
1358
+ * `tagRef` that lived here, `_ruleName` plus the untyped-`node()` type).
1359
+ * - the ambient `trivia` / `scanSkip` stamps and the `hostMode` / `trackLines`
1360
+ * stamps, which `plugin/index.ts` was applying itself in three more loops
1361
+ * with a comment saying it had to "because the macro never calls `rules()`".
1362
+ * - `markUnusedValues`, `RULE_ORDER`, and the grammar reflection.
1363
+ *
1364
+ * WHAT THIS CHANGES ABOUT THE RESULT, deliberately: a key the factory never
1365
+ * referenced through `g` now comes back as the parser itself rather than a
1366
+ * `lazy` wrapping it, because that is what `rules()` produces and the runtime
1367
+ * shape is the one the encoder is measured good on. A key that IS referenced
1368
+ * still comes back as its placeholder, so recursion is unchanged.
1369
+ *
1370
+ * A failure inside the factory cannot `return null` from here — it is running
1371
+ * under `rules()` — so it throws `ABORT` and is caught below. `rules()`'s own
1372
+ * self-alias `Error` is caught by the same handler, preserving this function's
1373
+ * "return null and let the caller fall back to the interpreter" contract rather
1374
+ * than turning a tolerated shape into a build failure.
1375
+ */
1376
+ const ABORT = Symbol('parseman: factory not statically evaluable')
1377
+ let built: Record<string, Combinator<unknown>>
1378
+ try {
1379
+ built = rules(options ?? {}, (g: Record<string, Combinator<unknown>>) => {
1380
+ // Outer ScopeEntry values carry their mfSrcs and are replayed by scopeGet()
1381
+ // when body statements or return expressions reference them.
1382
+ const localScope: XScope = new Map(scope as XScope)
1383
+ localScope.set(proxyName, g)
1384
+
1385
+ // ── Phase 1: the factory's own body statements ────────────────────────
1386
+ if (!evalBodyStatements(statements, localScope, code, out)) throw ABORT
1387
+
1388
+ // ── Phase 2: flatten the return object → dedup last-wins → evaluate ────
1389
+ // `flattenRuleEntries` returns the ordered (key, valueExpr, scope). A later
1390
+ // property of the same name wins.
1391
+ const entries = flattenRuleEntries(retObj as unknown as ObjectExpression, localScope, code)
1392
+ if (!entries) throw ABORT
1393
+ const finalByKey = new Map<string, RuleEntry>()
1394
+ for (const e of entries) finalByKey.set(e.key, e) // keeps first position, updates value → last wins
1395
+ const definitions: Record<string, Combinator<unknown>> = {}
1396
+ for (const [key, e] of finalByKey) {
1397
+ const val = anyValue(e.value, e.scope, e.code, mapFnSources)
1398
+ if (!isCombinator(val)) throw ABORT
1399
+ definitions[key] = val as Combinator<unknown>
1400
+ }
1401
+ return definitions
1402
+ }) as unknown as Record<string, Combinator<unknown>>
1403
+ } catch (e) {
1404
+ if (e === ABORT) return null
1405
+ // `rules()` throws on a rule that is a direct alias to itself. This function's
1406
+ // contract is `null` — "leave it interpreted" — not a thrown build failure, and
1407
+ // the interpreter accepts the same shape, so the caller's existing fallback is
1408
+ // the right answer. Anything else is a real defect and must not be swallowed.
1409
+ if (e instanceof Error && /cannot be a direct alias to itself/.test(e.message)) return null
1410
+ throw e
1411
+ }
1412
+
1413
+ /*
1414
+ * DECLARED KEYS ONLY, in DECLARATION order.
1415
+ *
1416
+ * `rules()` returns its whole cache, which also holds a placeholder for every
1417
+ * EXTERNAL name the factory touched — a `g.X` provided by another piece. Those
1418
+ * are references, not rules of this map, and handing them back would mint
1419
+ * `rule:` ids this grammar does not define, widen the coverage denominator and
1420
+ * put an undefined slot in the emitted map. The previous implementation
1421
+ * returned only its own `ruleRefs` for the same reason; this preserves that
1422
+ * contract exactly while letting `rules()` own everything else.
1423
+ */
1424
+ const map = new Map<string, Combinator<unknown>>()
1425
+ for (const key of keys) map.set(key, built[key]!)
1426
+ return map
1427
+ }
1428
+
1429
+ /** A combinator slot created by ref() — has a callable `define`. */
1430
+ type DefinableRef = Combinator<unknown> & { define(p: Combinator<unknown>): void }
1431
+
1432
+ function isDefinableRef(v: unknown): v is DefinableRef {
1433
+ return isCombinator(v)
1434
+ && typeof (v as { define?: unknown }).define === 'function'
1435
+ && (v as { _def: { tag?: string } })._def.tag === 'lazy'
1436
+ }
1437
+
1438
+ /**
1439
+ * If `init` is a bare `ref()` call, evaluate it to a real ref placeholder and
1440
+ * register it in scope under `name`. Returns the ref, or null if `init` isn't
1441
+ * a `ref()` call. Used by the macro pre-pass so standalone refs resolve before
1442
+ * compilation (parity with the interpreter / compile()).
1443
+ */
1444
+ export function evaluateRefDeclaration(
1445
+ init: Expression,
1446
+ name: string,
1447
+ scope: Scope,
1448
+ ): DefinableRef | null {
1449
+ if (init.type !== 'CallExpression') return null
1450
+ const callee = (init as unknown as { callee: { type: string; name?: string } }).callee
1451
+ if (callee.type !== 'Identifier' || callee.name !== 'ref') return null
1452
+ if ((init as unknown as { arguments: unknown[] }).arguments.length !== 0) return null
1453
+ const slot = ref<unknown>() as DefinableRef
1454
+ ;(scope as XScope).set(name, { combi: slot, mfSrcs: [] } satisfies ScopeEntry)
1455
+ return slot
1456
+ }
1457
+
1458
+ /**
1459
+ * Apply a `someRef.define(expr)` statement: resolve the target ref from scope,
1460
+ * evaluate the argument to a combinator, and call `.define()`. Returns true on
1461
+ * success. The macro removes the original statement from the output (it would
1462
+ * otherwise reference the stripped import); returning false signals "leave it".
1463
+ */
1464
+ export function applyDefineStatement(
1465
+ callExpr: Expression,
1466
+ scope: Scope,
1467
+ code: string,
1468
+ ): boolean {
1469
+ if (callExpr.type !== 'CallExpression') return false
1470
+ const callee = (callExpr as unknown as { callee: { type: string } }).callee
1471
+ if (callee.type !== 'MemberExpression') return false
1472
+ const mem = callee as unknown as { object: { type: string; name?: string }; property: { type: string; name?: string }; computed: boolean }
1473
+ if (mem.computed) return false
1474
+ if (mem.property.type !== 'Identifier' || mem.property.name !== 'define') return false
1475
+ if (mem.object.type !== 'Identifier' || !mem.object.name) return false
1476
+
1477
+ const target = (scope as XScope).get(mem.object.name)
1478
+ const refCombi = isScopeEntry(target) ? target.combi : (isCombinator(target) ? target : null)
1479
+ if (!refCombi || !isDefinableRef(refCombi)) return false
1480
+
1481
+ const args = (callExpr as unknown as { arguments: Array<{ type: string }> }).arguments
1482
+ if (args.length !== 1 || args[0]!.type === 'SpreadElement') return false
1483
+ const inner = anyValue(args[0] as unknown as Expression, scope as XScope, code, [])
1484
+ if (!isCombinator(inner)) return false
1485
+
1486
+ try { refCombi.define(inner) } catch { return false }
1487
+ return true
1488
+ }
1489
+
1490
+ /** Check if an AST node references any name from the given scope or names set. */
1491
+ export function referencesAny(node: Node, names: Set<string>, scope: Scope): boolean {
1492
+ if (node.type === 'Identifier') {
1493
+ return names.has(node.name) || scope.has(node.name)
1494
+ }
1495
+ for (const key of Object.keys(node) as (keyof typeof node)[]) {
1496
+ const child = node[key]
1497
+ if (!child || typeof child !== 'object') continue
1498
+ if (Array.isArray(child)) {
1499
+ if (child.some(c => c && typeof c === 'object' && 'type' in c && referencesAny(c as Node, names, scope))) return true
1500
+ } else if ('type' in child) {
1501
+ if (referencesAny(child as Node, names, scope)) return true
1502
+ }
1503
+ }
1504
+ return false
1505
+ }