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,606 @@
1
+ /**
2
+ * Resolve a `node(..., build)` reducer expression to the function it names, so the
3
+ * capture-cost analysis reads a REAL parameter list instead of failing open.
4
+ *
5
+ * `buildSrc` is the source text of the EXPRESSION at the call site. An inline arrow is
6
+ * self-describing, but every other spelling — a bare identifier, a namespaced
7
+ * `helpers.fold`, an import from a shared module — carries no parameter list at all, and
8
+ * the analysis used to give up on all of them and charge the node all five capture tiers.
9
+ *
10
+ * Those spellings are not exotic; sharing reducers across a grammar and importing them
11
+ * from another module is ordinary grammar authoring. So this module does the work rather
12
+ * than reporting that it won't:
13
+ *
14
+ * - REAL SCOPE ANALYSIS. A lexical scope tree over the module, resolved by the source
15
+ * OFFSET of the identifier at the call site. Shadowing is decidable, so it is decided:
16
+ * an inner binding of the same name resolves to the inner binding, and only genuinely
17
+ * unreadable bindings decline. (The previous rule — "decline any name bound more than
18
+ * once anywhere in the module" — declined correct code to avoid thinking about this.)
19
+ * - `let` / `var`, admitted when the binding is never reassigned. Reassignment is
20
+ * detected, not assumed.
21
+ * - CROSS-MODULE imports: named, default, namespace-member, aliased, and re-exported.
22
+ * Modules are resolved and parsed once and cached by path + mtime.
23
+ * - ALIASES: `const fold = foldOperation` follows to the target.
24
+ * - AST-based parameter counting, so a DEFAULT (`(c, f = undefined, s, r) =>`, arity 4)
25
+ * or a destructured parameter is counted positionally instead of defeating a regex.
26
+ *
27
+ * What still declines, because it is genuinely undecidable rather than merely unread:
28
+ * a REST parameter (`...args` — the declared arity is unbounded), a body that references
29
+ * `arguments`, and a reducer that is computed, conditionally assigned, or reassigned. For
30
+ * those the `build-arity-unconfirmed` diagnostic fires, and `node(..., { buildArity: n })`
31
+ * lets the author declare the answer so the fail-open path is a true last resort.
32
+ */
33
+ import * as fs from 'node:fs'
34
+ import * as path from 'node:path'
35
+ import { createRequire } from 'node:module'
36
+ import { parseSync } from 'oxc-parser'
37
+
38
+ type AnyNode = { type: string; start: number; end: number } & Record<string, unknown>
39
+
40
+ /** Why a reducer could not be reduced to a parameter list. `undefined` = it could. */
41
+ export type UnresolvedReason =
42
+ | 'rest-parameter'
43
+ | 'arguments'
44
+ | 'reassigned'
45
+ | 'not-a-function'
46
+ | 'unresolved-import'
47
+ | 'not-found'
48
+ | 'computed'
49
+ | 'foreign-source'
50
+ /** Two DISTINCT registered files hold byte-identical text, so the text no longer
51
+ * identifies the module an offset indexes. See `register`. */
52
+ | 'ambiguous-source'
53
+
54
+ export type ResolvedReducer = {
55
+ /** Declared positional arity, or `null` when genuinely undecidable. */
56
+ arity: number | null
57
+ /** Source text of the resolved function, for shape analysis (inline-`mk`). */
58
+ src: string | null
59
+ reason?: UnresolvedReason
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Scope tree
64
+ // ---------------------------------------------------------------------------
65
+
66
+ type BindingKind = 'const' | 'let' | 'var' | 'function' | 'class' | 'param' | 'import' | 'catch'
67
+
68
+ type ImportRef = { specifier: string; imported: string | '*' | 'default' }
69
+
70
+ type Binding = {
71
+ kind: BindingKind
72
+ /** Initializer / declaration node, when the binding names one directly. */
73
+ init?: AnyNode
74
+ /** Set for an `import` binding. */
75
+ importRef?: ImportRef
76
+ /** Any assignment or update to this name anywhere in its scope subtree. */
77
+ reassigned?: boolean
78
+ }
79
+
80
+ type ScopeNode = {
81
+ start: number
82
+ end: number
83
+ bindings: Map<string, Binding>
84
+ children: ScopeNode[]
85
+ parent: ScopeNode | null
86
+ /** `var` and function declarations hoist to the nearest scope with this set. */
87
+ isFunction: boolean
88
+ }
89
+
90
+ const FUNCTION_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])
91
+ const SCOPE_TYPES = new Set([
92
+ ...FUNCTION_TYPES, 'BlockStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement',
93
+ 'CatchClause', 'ClassDeclaration', 'ClassExpression', 'StaticBlock',
94
+ ])
95
+
96
+ function newScope(start: number, end: number, parent: ScopeNode | null, isFunction: boolean): ScopeNode {
97
+ const s: ScopeNode = { start, end, bindings: new Map(), children: [], parent, isFunction }
98
+ parent?.children.push(s)
99
+ return s
100
+ }
101
+
102
+ /** Every identifier a binding pattern introduces. */
103
+ function patternNames(pat: unknown, out: string[]): void {
104
+ if (!pat || typeof pat !== 'object') return
105
+ const n = pat as AnyNode
106
+ switch (n.type) {
107
+ case 'Identifier': out.push(n.name as string); return
108
+ case 'AssignmentPattern': return patternNames(n.left, out)
109
+ case 'RestElement': return patternNames(n.argument, out)
110
+ case 'ArrayPattern': for (const el of (n.elements as unknown[]) ?? []) patternNames(el, out); return
111
+ case 'ObjectPattern':
112
+ for (const p of (n.properties as AnyNode[]) ?? []) patternNames(p.type === 'RestElement' ? p.argument : p.value, out)
113
+ return
114
+ // A TS parameter property / assignment target wrapper.
115
+ case 'TSParameterProperty': return patternNames(n.parameter, out)
116
+ default: return
117
+ }
118
+ }
119
+
120
+ function declare(scope: ScopeNode, name: string, b: Binding): void {
121
+ // First declaration wins; a duplicate `var` re-declaration does not change the shape.
122
+ if (!scope.bindings.has(name)) scope.bindings.set(name, b)
123
+ }
124
+
125
+ /** `var` and function declarations hoist to the nearest FUNCTION scope, not the block. */
126
+ function functionScope(scope: ScopeNode): ScopeNode {
127
+ let s: ScopeNode = scope
128
+ while (!s.isFunction && s.parent) s = s.parent
129
+ return s
130
+ }
131
+
132
+ function buildScopeTree(body: unknown[], moduleEnd: number): ScopeNode {
133
+ const root = newScope(0, moduleEnd, null, true)
134
+
135
+ const declarePattern = (scope: ScopeNode, pat: unknown, kind: BindingKind, init?: AnyNode): void => {
136
+ const names: string[] = []
137
+ patternNames(pat, names)
138
+ const target = kind === 'var' ? functionScope(scope) : scope
139
+ for (const name of names) {
140
+ declare(target, name, init !== undefined && names.length === 1 ? { kind, init } : { kind })
141
+ }
142
+ }
143
+
144
+ const walk = (n: unknown, scope: ScopeNode): void => {
145
+ if (!n || typeof n !== 'object') return
146
+ if (Array.isArray(n)) { for (const item of n) walk(item, scope); return }
147
+ const node = n as AnyNode
148
+ if (typeof node.type !== 'string') return
149
+
150
+ // --- declarations, in the CURRENT scope ---
151
+ switch (node.type) {
152
+ case 'VariableDeclaration': {
153
+ const kind = (node.kind as BindingKind) ?? 'var'
154
+ for (const d of (node.declarations as AnyNode[]) ?? []) {
155
+ declarePattern(scope, d.id, kind, (d.init as AnyNode | null) ?? undefined)
156
+ }
157
+ break
158
+ }
159
+ case 'FunctionDeclaration':
160
+ if (node.id) declare(functionScope(scope), (node.id as AnyNode).name as string, { kind: 'function', init: node })
161
+ break
162
+ case 'ClassDeclaration':
163
+ if (node.id) declare(scope, (node.id as AnyNode).name as string, { kind: 'class' })
164
+ break
165
+ case 'ImportDeclaration': {
166
+ const specifier = ((node.source as AnyNode | undefined)?.value as string) ?? ''
167
+ for (const sp of (node.specifiers as AnyNode[]) ?? []) {
168
+ const local = (sp.local as AnyNode | undefined)?.name as string | undefined
169
+ if (!local) continue
170
+ const imported = sp.type === 'ImportDefaultSpecifier'
171
+ ? 'default'
172
+ : sp.type === 'ImportNamespaceSpecifier'
173
+ ? '*'
174
+ : ((sp.imported as AnyNode | undefined)?.name as string) ?? local
175
+ declare(scope, local, { kind: 'import', importRef: { specifier, imported } })
176
+ }
177
+ break
178
+ }
179
+ default: break
180
+ }
181
+
182
+ // --- new scopes ---
183
+ if (SCOPE_TYPES.has(node.type)) {
184
+ const isFn = FUNCTION_TYPES.has(node.type)
185
+ const inner = newScope(node.start, node.end, scope, isFn)
186
+ if (isFn) {
187
+ for (const p of (node.params as unknown[]) ?? []) declarePattern(inner, p, 'param')
188
+ // A named function EXPRESSION binds its own name inside its body.
189
+ if (node.type === 'FunctionExpression' && node.id) {
190
+ declare(inner, (node.id as AnyNode).name as string, { kind: 'function', init: node })
191
+ }
192
+ }
193
+ if (node.type === 'CatchClause' && node.param) declarePattern(inner, node.param, 'catch')
194
+ for (const key of Object.keys(node)) {
195
+ if (key === 'type' || key === 'start' || key === 'end') continue
196
+ walk(node[key], inner)
197
+ }
198
+ return
199
+ }
200
+
201
+ for (const key of Object.keys(node)) {
202
+ if (key === 'type' || key === 'start' || key === 'end') continue
203
+ walk(node[key], scope)
204
+ }
205
+ }
206
+
207
+ for (const stmt of body) walk(stmt, root)
208
+ return root
209
+ }
210
+
211
+ /** Innermost scope containing `offset`. */
212
+ function scopeAt(root: ScopeNode, offset: number): ScopeNode {
213
+ let cur = root
214
+ for (;;) {
215
+ const next = cur.children.find(c => offset >= c.start && offset <= c.end)
216
+ if (!next) return cur
217
+ cur = next
218
+ }
219
+ }
220
+
221
+ function lookup(scope: ScopeNode, name: string): { binding: Binding; scope: ScopeNode } | null {
222
+ for (let s: ScopeNode | null = scope; s; s = s.parent) {
223
+ const b = s.bindings.get(name)
224
+ if (b) return { binding: b, scope: s }
225
+ }
226
+ return null
227
+ }
228
+
229
+ /**
230
+ * Mark every binding that is assigned or updated after declaration. This is what makes a
231
+ * `let` admissible: "never reassigned" is a decidable property, and declining every
232
+ * `let` because one of them *might* be reassigned is the guesswork this module exists to
233
+ * remove.
234
+ */
235
+ function markReassignments(body: unknown[], root: ScopeNode): void {
236
+ const walk = (n: unknown): void => {
237
+ if (!n || typeof n !== 'object') return
238
+ if (Array.isArray(n)) { for (const item of n) walk(item); return }
239
+ const node = n as AnyNode
240
+ if (typeof node.type !== 'string') return
241
+ if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
242
+ const target = (node.type === 'UpdateExpression' ? node.argument : node.left) as AnyNode | undefined
243
+ const names: string[] = []
244
+ if (target) patternNames(target, names)
245
+ for (const name of names) {
246
+ const hit = lookup(scopeAt(root, node.start), name)
247
+ if (hit) hit.binding.reassigned = true
248
+ }
249
+ }
250
+ for (const key of Object.keys(node)) {
251
+ if (key === 'type' || key === 'start' || key === 'end') continue
252
+ walk(node[key])
253
+ }
254
+ }
255
+ for (const stmt of body) walk(stmt)
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Parameter list → arity, from the AST
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Declared positional arity of a function node, or `null` when undecidable.
264
+ *
265
+ * A DEFAULT does not change positional arity (`(c, f = undefined, s, r)` is 4) and a
266
+ * DESTRUCTURED parameter still occupies one position — both were previously rejected by
267
+ * a regex that only accepted plain identifiers. A REST parameter genuinely is undecidable
268
+ * (the declared arity is unbounded), as is a body that reads `arguments`.
269
+ */
270
+ export function functionArity(fn: AnyNode): { arity: number | null; reason?: UnresolvedReason } {
271
+ const params = (fn.params as AnyNode[]) ?? []
272
+ let count = 0
273
+ for (const p of params) {
274
+ if (p.type === 'RestElement') return { arity: null, reason: 'rest-parameter' }
275
+ // A TypeScript `this` parameter is not a positional argument.
276
+ if (p.type === 'Identifier' && p.name === 'this') continue
277
+ count++
278
+ }
279
+ if (fn.type !== 'ArrowFunctionExpression' && referencesArguments(fn.body)) {
280
+ return { arity: null, reason: 'arguments' }
281
+ }
282
+ return { arity: count }
283
+ }
284
+
285
+ /** `arguments` in this function's own body — not a nested non-arrow function's. */
286
+ function referencesArguments(n: unknown): boolean {
287
+ if (!n || typeof n !== 'object') return false
288
+ if (Array.isArray(n)) return n.some(referencesArguments)
289
+ const node = n as AnyNode
290
+ if (typeof node.type !== 'string') return false
291
+ if (node.type === 'Identifier' && node.name === 'arguments') return true
292
+ // A nested non-arrow function has its own `arguments`; an arrow inherits ours.
293
+ if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') return false
294
+ for (const key of Object.keys(node)) {
295
+ if (key === 'type' || key === 'start' || key === 'end') continue
296
+ if (referencesArguments(node[key])) return true
297
+ }
298
+ return false
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Module graph
303
+ // ---------------------------------------------------------------------------
304
+
305
+ type AnalysedModule = {
306
+ file: string
307
+ src: string
308
+ body: unknown[]
309
+ scopes: ScopeNode
310
+ }
311
+
312
+ const _moduleCache = new Map<string, { mtimeMs: number; mod: AnalysedModule | null }>()
313
+
314
+ function analyseModuleFile(file: string): AnalysedModule | null {
315
+ let mtimeMs: number
316
+ try { mtimeMs = fs.statSync(file).mtimeMs } catch { return null }
317
+ const hit = _moduleCache.get(file)
318
+ if (hit && hit.mtimeMs === mtimeMs) return hit.mod
319
+ let mod: AnalysedModule | null = null
320
+ try {
321
+ const src = fs.readFileSync(file, 'utf8')
322
+ const r = parseSync(file, src)
323
+ if (r.errors.length === 0) {
324
+ const body = r.program.body as unknown[]
325
+ const scopes = buildScopeTree(body, src.length)
326
+ markReassignments(body, scopes)
327
+ mod = { file, src, body, scopes }
328
+ }
329
+ } catch { /* leave null */ }
330
+ _moduleCache.set(file, { mtimeMs, mod })
331
+ return mod
332
+ }
333
+
334
+ const SOURCE_EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx']
335
+
336
+ /**
337
+ * Resolve an import specifier to a readable file.
338
+ *
339
+ * Deliberately broader than `resolvePrivateSourceModule` in `index.ts`: that one
340
+ * restricts to the importing package because it LOWERS the target into the output. Here
341
+ * we only READ a parameter list, so a workspace sibling or an installed package is
342
+ * perfectly safe to inspect — and refusing to look is exactly how "imported reducer"
343
+ * became a permanent fail-open.
344
+ */
345
+ function resolveImport(fromFile: string, specifier: string): string | null {
346
+ if (specifier.startsWith('.') || path.isAbsolute(specifier)) {
347
+ const base = path.resolve(path.dirname(fromFile), specifier)
348
+ const ext = path.extname(base)
349
+ const stem = ext ? base.slice(0, -ext.length) : base
350
+ const candidates = [
351
+ ...(ext ? [base] : []),
352
+ // `./x.js` in TS source usually means `./x.ts`.
353
+ ...SOURCE_EXTS.map(e => `${stem}${e}`),
354
+ ...SOURCE_EXTS.map(e => path.join(base, `index${e}`)),
355
+ ]
356
+ for (const c of candidates) {
357
+ try { if (fs.statSync(c).isFile()) return fs.realpathSync(c) } catch { /* next */ }
358
+ }
359
+ return null
360
+ }
361
+ // A bare package specifier — a workspace sibling or an installed package. Node's own
362
+ // resolution from the importing file, which is what the built program will use.
363
+ try {
364
+ return fs.realpathSync(createRequire(fromFile).resolve(specifier))
365
+ } catch { return null }
366
+ }
367
+
368
+ /** Does this `export <decl>` actually introduce `name`? */
369
+ function declarationBinds(decl: AnyNode, name: string): boolean {
370
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
371
+ return ((decl.id as AnyNode | undefined)?.name as string | undefined) === name
372
+ }
373
+ if (decl.type === 'VariableDeclaration') {
374
+ const names: string[] = []
375
+ for (const d of (decl.declarations as AnyNode[]) ?? []) patternNames(d.id, names)
376
+ return names.includes(name)
377
+ }
378
+ return false
379
+ }
380
+
381
+ /** Find what a module EXPORTS under `name` ('default' for a default export). */
382
+ function exportedBinding(
383
+ mod: AnalysedModule,
384
+ name: string,
385
+ seen: Set<string>,
386
+ ): { mod: AnalysedModule; binding: Binding } | null {
387
+ for (const stmt of mod.body as AnyNode[]) {
388
+ if (stmt.type === 'ExportDefaultDeclaration' && name === 'default') {
389
+ const d = stmt.declaration as AnyNode
390
+ if (FUNCTION_TYPES.has(d.type)) return { mod, binding: { kind: 'const', init: d } }
391
+ if (d.type === 'Identifier') {
392
+ const hit = lookup(mod.scopes, d.name as string)
393
+ return hit ? { mod, binding: hit.binding } : null
394
+ }
395
+ return null
396
+ }
397
+ if (stmt.type === 'ExportNamedDeclaration') {
398
+ const decl = stmt.declaration as AnyNode | undefined
399
+ if (decl && declarationBinds(decl, name)) {
400
+ // `export const f = …` / `export function f() {}` — already in module scope.
401
+ const hit = lookup(mod.scopes, name)
402
+ if (hit && hit.scope === mod.scopes) return { mod, binding: hit.binding }
403
+ }
404
+ for (const sp of (stmt.specifiers as AnyNode[]) ?? []) {
405
+ const exported = ((sp.exported as AnyNode | undefined)?.name as string) ?? ''
406
+ if (exported !== name) continue
407
+ const local = ((sp.local as AnyNode | undefined)?.name as string) ?? exported
408
+ const from = (stmt.source as AnyNode | undefined)?.value as string | undefined
409
+ if (from !== undefined) return followReExport(mod, from, local, seen)
410
+ const hit = lookup(mod.scopes, local)
411
+ return hit ? { mod, binding: hit.binding } : null
412
+ }
413
+ }
414
+ // `export * from './y'` — search the target.
415
+ if (stmt.type === 'ExportAllDeclaration' && !stmt.exported) {
416
+ const from = (stmt.source as AnyNode | undefined)?.value as string | undefined
417
+ if (from === undefined) continue
418
+ const found = followReExport(mod, from, name, seen)
419
+ if (found) return found
420
+ }
421
+ }
422
+ // A plain module-scope declaration that the file exports elsewhere, or a module we
423
+ // parsed from compiled output where the export list is a bundler rename.
424
+ const hit = lookup(mod.scopes, name)
425
+ return hit && hit.scope === mod.scopes ? { mod, binding: hit.binding } : null
426
+ }
427
+
428
+ function followReExport(
429
+ mod: AnalysedModule,
430
+ specifier: string,
431
+ name: string,
432
+ seen: Set<string>,
433
+ ): { mod: AnalysedModule; binding: Binding } | null {
434
+ const file = resolveImport(mod.file, specifier)
435
+ if (!file || seen.has(`${file}\0${name}`)) return null
436
+ seen.add(`${file}\0${name}`)
437
+ const next = analyseModuleFile(file)
438
+ return next ? exportedBinding(next, name, seen) : null
439
+ }
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // The resolver
443
+ // ---------------------------------------------------------------------------
444
+
445
+ const MAX_HOPS = 12
446
+
447
+ export type ReducerResolver = {
448
+ /**
449
+ * Resolve the reducer expression at `offset` (its source text is `exprSrc`).
450
+ * Returns `null` when the expression is not a name at all (an inline function —
451
+ * the caller already has its source and needs nothing from us).
452
+ *
453
+ * `src` is the module text `offset` indexes. An offset is only meaningful against the
454
+ * source it came from, so the caller must say which one it has; see `resolve`'s
455
+ * implementation for what happens when it is not this resolver's.
456
+ */
457
+ resolve(exprSrc: string, offset: number, src: string): ResolvedReducer | null
458
+ /**
459
+ * Announce a module whose text the evaluator may later hand to `resolve` as `src` —
460
+ * an imported `rules()` factory is evaluated with `code: mod.src`, so its `node(…,
461
+ * build)` offsets index THAT file, not the entry file.
462
+ *
463
+ * Registering it makes those offsets answerable against the module's own scope tree.
464
+ * Without it `resolve` can only decline (`foreign-source`), which fails open to full
465
+ * capture — correct, but it charges every such node all five facilities.
466
+ *
467
+ * Returns whether the file could be analysed; an unreadable or unparseable module
468
+ * simply stays unregistered and keeps declining.
469
+ */
470
+ register(file: string): boolean
471
+ }
472
+
473
+ const IDENT_RE = /^[A-Za-z_$][\w$]*$/
474
+ const MEMBER_RE = /^([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)$/
475
+
476
+ export function createReducerResolver(entryFile: string, body: unknown[], src: string): ReducerResolver {
477
+ const scopes = buildScopeTree(body, src.length)
478
+ markReassignments(body, scopes)
479
+ const entry: AnalysedModule = { file: entryFile, src, body, scopes }
480
+
481
+ // Modules OTHER than the entry file whose offsets `resolve` may be handed, keyed by
482
+ // the module text itself — that is the only identity the evaluator carries, since it
483
+ // threads `code` (a string) and not a path.
484
+ const registered = new Map<string, AnalysedModule>()
485
+ // Texts claimed by more than one DISTINCT file. Identical text is identical for
486
+ // offsets, but NOT for import resolution: `fromBinding` follows a hop with
487
+ // `resolveImport(mod.file, specifier)`, and identical text means identical RELATIVE
488
+ // specifiers, which resolve to different absolute files from different directories.
489
+ // Letting the second registration overwrite the first would answer such an offset
490
+ // against the wrong module and yield a wrong arity — and a wrong arity UNDER-captures,
491
+ // calling the reducer with arguments the compiler elided. That is the exact failure
492
+ // this registry exists to prevent, so an ambiguous key declines instead.
493
+ const ambiguous = new Set<string>()
494
+
495
+ const fromBinding = (
496
+ mod: AnalysedModule,
497
+ binding: Binding,
498
+ memberName: string | undefined,
499
+ hops: number,
500
+ seen: Set<string>,
501
+ ): ResolvedReducer => {
502
+ if (hops > MAX_HOPS) return { arity: null, src: null, reason: 'not-found' }
503
+
504
+ // A reassigned binding may hold a different function by the time it is read.
505
+ if (binding.reassigned) return { arity: null, src: null, reason: 'reassigned' }
506
+
507
+ if (binding.kind === 'param' || binding.kind === 'catch' || binding.kind === 'class') {
508
+ return { arity: null, src: null, reason: 'not-a-function' }
509
+ }
510
+
511
+ if (binding.kind === 'import' && binding.importRef) {
512
+ const { specifier, imported } = binding.importRef
513
+ // `import * as ns` used without a member names a MODULE, not a function. Decide
514
+ // that before touching the filesystem, so the reason is the real one rather than
515
+ // whatever the resolver happened to say about the path.
516
+ if (imported === '*' && memberName === undefined) return { arity: null, src: null, reason: 'not-a-function' }
517
+ const file = resolveImport(mod.file, specifier)
518
+ if (!file) return { arity: null, src: null, reason: 'unresolved-import' }
519
+ const next = analyseModuleFile(file)
520
+ if (!next) return { arity: null, src: null, reason: 'unresolved-import' }
521
+ // `import * as helpers` + `helpers.fold` — the member names the export.
522
+ const wanted = imported === '*' ? memberName : imported
523
+ if (wanted === undefined) return { arity: null, src: null, reason: 'not-a-function' }
524
+ const found = exportedBinding(next, wanted, seen)
525
+ if (!found) return { arity: null, src: null, reason: 'not-found' }
526
+ return fromBinding(found.mod, found.binding, undefined, hops + 1, seen)
527
+ }
528
+
529
+ const init = binding.init
530
+ if (!init) return { arity: null, src: null, reason: 'not-a-function' }
531
+
532
+ // `const fold = foldOperation` — follow the alias.
533
+ if (init.type === 'Identifier') {
534
+ const hit = lookup(scopeAt(mod.scopes, init.start), init.name as string)
535
+ if (!hit) return { arity: null, src: null, reason: 'not-found' }
536
+ return fromBinding(mod, hit.binding, undefined, hops + 1, seen)
537
+ }
538
+ // `const fold = helpers.foldOperation`. A COMPUTED access (`table[key]`) names
539
+ // something only the running program knows — that is genuine undecidability, and it
540
+ // reports as such rather than as a missing binding.
541
+ if (init.type === 'ComputedMemberExpression' || (init.computed === true && init.type.endsWith('MemberExpression'))) {
542
+ return { arity: null, src: null, reason: 'computed' }
543
+ }
544
+ if (init.type === 'StaticMemberExpression' || init.type === 'MemberExpression') {
545
+ const obj = init.object as AnyNode | undefined
546
+ const prop = init.property as AnyNode | undefined
547
+ if (obj?.type === 'Identifier' && prop?.type === 'Identifier') {
548
+ const hit = lookup(scopeAt(mod.scopes, init.start), obj.name as string)
549
+ if (!hit) return { arity: null, src: null, reason: 'not-found' }
550
+ return fromBinding(mod, hit.binding, prop.name as string, hops + 1, seen)
551
+ }
552
+ return { arity: null, src: null, reason: 'computed' }
553
+ }
554
+
555
+ if (!FUNCTION_TYPES.has(init.type)) return { arity: null, src: null, reason: 'not-a-function' }
556
+
557
+ const { arity, reason } = functionArity(init)
558
+ return { arity, src: mod.src.slice(init.start, init.end), ...(reason ? { reason } : {}) }
559
+ }
560
+
561
+ return {
562
+ register(file) {
563
+ const mod = analyseModuleFile(file)
564
+ if (!mod) return false
565
+ // The entry file needs no entry — it is matched first, and its `body`/`src` came
566
+ // from the caller rather than a re-read, so they are the authoritative pair.
567
+ if (mod.src === src) return true
568
+ const prior = registered.get(mod.src)
569
+ if (prior !== undefined && prior.file !== mod.file) {
570
+ ambiguous.add(mod.src)
571
+ registered.delete(mod.src)
572
+ return false
573
+ }
574
+ if (!ambiguous.has(mod.src)) registered.set(mod.src, mod)
575
+ return !ambiguous.has(mod.src)
576
+ },
577
+
578
+ resolve(exprSrc, offset, offsetSrc) {
579
+ const text = exprSrc.trim()
580
+ const ident = IDENT_RE.test(text) ? text : null
581
+ const member = ident ? null : MEMBER_RE.exec(text)
582
+ if (!ident && !member) return null // an inline function, or something computed
583
+
584
+ // An IMPORTED `rules()` factory is evaluated with `code: mod.src` (plugin/index.ts),
585
+ // so a `node(..., build)` inside it carries an offset into THAT file. Indexing it
586
+ // into the entry file's scope tree is not a near miss: it silently names whatever
587
+ // binding happens to live at the same absolute offset, and a wrong arity UNDER-
588
+ // captures — the reducer is then called with arguments the compiler elided.
589
+ //
590
+ // So the offset picks its OWN module: the entry file, or one the plugin registered
591
+ // when it decided to evaluate a factory out of it. An offset from anywhere else is
592
+ // still refused, which fails open to full capture and says so on the degradation
593
+ // channel — never answered against the wrong scope tree.
594
+ const mod = offsetSrc === src ? entry : registered.get(offsetSrc)
595
+ if (!mod) {
596
+ return { arity: null, src: null, reason: ambiguous.has(offsetSrc) ? 'ambiguous-source' : 'foreign-source' }
597
+ }
598
+
599
+ const name = ident ?? member![1]!
600
+ const memberName = ident ? undefined : member![2]!
601
+ const hit = lookup(scopeAt(mod.scopes, offset), name)
602
+ if (!hit) return { arity: null, src: null, reason: 'not-found' }
603
+ return fromBinding(mod, hit.binding, memberName, 0, new Set())
604
+ },
605
+ }
606
+ }