@backbencher/brain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-KY7K552E.js +17 -0
  8. package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-2BSKD7O3.js +40 -0
  11. package/dist/chunk-2DE27DVC.js +1314 -0
  12. package/dist/chunk-2RRBMNBQ.js +322 -0
  13. package/dist/chunk-2RTN2DFU.js +711 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3LFTSMIV.js +174 -0
  17. package/dist/chunk-47HKX4AJ.js +930 -0
  18. package/dist/chunk-4H22TLOT.js +543 -0
  19. package/dist/chunk-5SD45FLC.js +818 -0
  20. package/dist/chunk-66A5ZFYA.js +1758 -0
  21. package/dist/chunk-6AIRWAO2.js +247 -0
  22. package/dist/chunk-6WNGD3NN.js +196 -0
  23. package/dist/chunk-7AN7U5IZ.js +371 -0
  24. package/dist/chunk-BHB7BYYI.js +626 -0
  25. package/dist/chunk-BQ3OVQ3P.js +125 -0
  26. package/dist/chunk-CW4XXSNN.js +1479 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DYM4FQEE.js +90 -0
  30. package/dist/chunk-EWMWK7WN.js +75 -0
  31. package/dist/chunk-FVVQTSIM.js +3427 -0
  32. package/dist/chunk-FWSL4VG6.js +1290 -0
  33. package/dist/chunk-GBI7V7KY.js +69 -0
  34. package/dist/chunk-GEFFO6O2.js +152 -0
  35. package/dist/chunk-HOWAQXJO.js +236 -0
  36. package/dist/chunk-HQSPJDHI.js +613 -0
  37. package/dist/chunk-IFZNR5UJ.js +2863 -0
  38. package/dist/chunk-J4PMBXQQ.js +336 -0
  39. package/dist/chunk-KAET47ZL.js +125 -0
  40. package/dist/chunk-KCDECRJY.js +440 -0
  41. package/dist/chunk-KLK3PBTO.js +1119 -0
  42. package/dist/chunk-L5TDYKYK.js +76 -0
  43. package/dist/chunk-MLKGABMK.js +9 -0
  44. package/dist/chunk-OWISBAOF.js +61 -0
  45. package/dist/chunk-PJTNGD3Z.js +6924 -0
  46. package/dist/chunk-QLK6ED4Q.js +235 -0
  47. package/dist/chunk-R2GW4F6R.js +19 -0
  48. package/dist/chunk-RCMTFSEF.js +87 -0
  49. package/dist/chunk-RH7KC5HU.js +12050 -0
  50. package/dist/chunk-RM3PY6TG.js +376 -0
  51. package/dist/chunk-ROMM6KMC.js +433 -0
  52. package/dist/chunk-SK6YVHFZ.js +211 -0
  53. package/dist/chunk-SM7RFQRO.js +214 -0
  54. package/dist/chunk-UFE77L67.js +782 -0
  55. package/dist/chunk-UGR4QIGS.js +67 -0
  56. package/dist/chunk-UUNHHX4M.js +13 -0
  57. package/dist/chunk-V44PGPA6.js +2104 -0
  58. package/dist/chunk-VL52PL2T.js +71 -0
  59. package/dist/chunk-VURYFIHW.js +201 -0
  60. package/dist/chunk-XJKJYIPP.js +7436 -0
  61. package/dist/chunk-YI6FF2ZR.js +340 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-642F7E5V.js +593 -0
  65. package/dist/database-TCTZDLE5.js +9 -0
  66. package/dist/doctor-GXCKSJOX.js +395 -0
  67. package/dist/edit-engine-KM7MPFEC.js +51 -0
  68. package/dist/engine-42TZFNIZ.js +11 -0
  69. package/dist/engine-LO6L4JLQ.js +18 -0
  70. package/dist/fingerprint-XDXFRWO6.js +33 -0
  71. package/dist/framework-intelligence-DWFCKV44.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-PL5YA7BA.js +21 -0
  74. package/dist/graph_request-MM4REP6H.js +350 -0
  75. package/dist/impact_request-WDE47IZ5.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-6BD7UBHR.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-4PW3AFXP.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-ZS5TS2Z6.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-DGZBIZS7.js +355 -0
  90. package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
  91. package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
  92. package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
  93. package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
  94. package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
  95. package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
  96. package/dist/real-css-analyzer-HPU277Q6.js +15 -0
  97. package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
  98. package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
  99. package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
  100. package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
  101. package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
  102. package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
  103. package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
  104. package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
  105. package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
  106. package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
  107. package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
  108. package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
  109. package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
  110. package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
  111. package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
  112. package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
  113. package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
  114. package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
  115. package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
  116. package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
  117. package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
  118. package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
  119. package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
  120. package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
  121. package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
  122. package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
  123. package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
  124. package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
  125. package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
  126. package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
  127. package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
  128. package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
  129. package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
  130. package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
  131. package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
  132. package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
  133. package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
  134. package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
  135. package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
  136. package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
  137. package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
  138. package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
  139. package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
  140. package/dist/regex-search-OPTLKUN5.js +239 -0
  141. package/dist/rules-I453JTRC.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-Y5GB5KXA.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-KB4MU4SI.js +16 -0
  146. package/dist/taint-YOTN5X55.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-G4NAGJIH.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-RF675A3C.js +32 -0
  151. package/dist/type-inference-4Y7WTMFE.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-LXGNFO37.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,3603 @@
1
+ import {
2
+ findDeclaringParent
3
+ } from "./chunk-OWISBAOF.js";
4
+ import {
5
+ ASTEngine
6
+ } from "./chunk-V44PGPA6.js";
7
+ import "./chunk-BQ3OVQ3P.js";
8
+ import "./chunk-SK6YVHFZ.js";
9
+ import "./chunk-2BSKD7O3.js";
10
+ import "./chunk-KAET47ZL.js";
11
+ import "./chunk-KCDECRJY.js";
12
+ import "./chunk-MLKGABMK.js";
13
+
14
+ // src/ast/real-nim-analyzer.ts
15
+ var RealNimAnalyzer = class {
16
+ analyze(rootNode, content, filePath, context) {
17
+ const symbols = [];
18
+ const relationships = [];
19
+ const imports = [];
20
+ const exports = [];
21
+ const errors = [];
22
+ const fileValues = /* @__PURE__ */ new Set();
23
+ const fileTypes = /* @__PURE__ */ new Set();
24
+ const fileCallables = /* @__PURE__ */ new Set();
25
+ const nestedCallables = /* @__PURE__ */ new Set();
26
+ const fileEnumMembers = /* @__PURE__ */ new Set();
27
+ const fileFieldNames = /* @__PURE__ */ new Set();
28
+ const genericParamNames = /* @__PURE__ */ new Set();
29
+ const importQualifiers = /* @__PURE__ */ new Set();
30
+ const declaredSubtypes = /* @__PURE__ */ new Map();
31
+ const docCommentByStartLine = /* @__PURE__ */ new Map();
32
+ const fileComplexity = this.collectDocModulesAndComplexity(
33
+ rootNode,
34
+ docCommentByStartLine,
35
+ imports,
36
+ exports,
37
+ importQualifiers,
38
+ relationships
39
+ );
40
+ this.collectDeclarations(rootNode, {
41
+ symbols,
42
+ errors,
43
+ docCommentByStartLine,
44
+ fileValues,
45
+ fileTypes,
46
+ fileCallables,
47
+ nestedCallables,
48
+ fileEnumMembers,
49
+ fileFieldNames,
50
+ genericParamNames,
51
+ declaredSubtypes
52
+ });
53
+ this.publishModuleSymbol(rootNode, filePath, symbols);
54
+ this.collectContainment(symbols, relationships);
55
+ this.collectPragmaDecorates(rootNode, symbols, relationships);
56
+ this.collectInheritance(symbols, relationships, fileTypes);
57
+ this.collectContractRefinement(rootNode, symbols, relationships);
58
+ this.pruneForeignReExports(symbols, exports);
59
+ this.collectOverrides(symbols, relationships, declaredSubtypes, context?.overrideCompanion);
60
+ this.collectCallsAndUses(rootNode, relationships, {
61
+ fileValues,
62
+ fileTypes,
63
+ fileCallables,
64
+ nestedCallables,
65
+ fileEnumMembers,
66
+ fileFieldNames,
67
+ genericParamNames,
68
+ importQualifiers
69
+ });
70
+ this.classifyEvidenceTier(relationships);
71
+ return {
72
+ language: "nim",
73
+ symbols,
74
+ relationships,
75
+ imports,
76
+ exports,
77
+ errors,
78
+ metadata: {
79
+ complexity: fileComplexity,
80
+ cyclomaticComplexity: fileComplexity,
81
+ linesOfCode: content.split("\n").length,
82
+ patterns: [],
83
+ framework: "Nim",
84
+ hasFrameworkIntelligence: true,
85
+ hasTypeInference: true
86
+ }
87
+ };
88
+ }
89
+ // ═══════════════════════════════════════════════════════════════════════
90
+ // 1. DOCUMENTATION COMMENTS
91
+ // ═══════════════════════════════════════════════════════════════════════
92
+ /**
93
+ * `##` and `##[` are DISTINCT node types from `#` in this grammar
94
+ * (`documentation_comment` / `block_documentation_comment` vs `comment`), and
95
+ * both are `extra` — so the parser places each one relative to the construct
96
+ * it documents in one of TWO ways, and reading only the first is why docstrings
97
+ * measured 0 through the real CLI:
98
+ *
99
+ * 1. ABSORBED. A `##` line that sits between a declaration's `proc` keyword
100
+ * and its body (the overwhelmingly common shape for a documented proc:
101
+ * `proc f()` / `## doc` / ` body`) becomes a CHILD of the declaration
102
+ * node, so its `nextNamedSibling` is the body's `statement_list`, not the
103
+ * declaration. Anchoring to the sibling in that case documents nothing.
104
+ * Measured: the doc for `proc newPoint` attached to a `statement_list`.
105
+ * 2. SIBLING. A `##` line above a whole top-level construct
106
+ * (`## doc` / `proc f() = body`) is a child of the enclosing scope
107
+ * (`source_file` or a `type_section`) and its `nextNamedSibling` IS the
108
+ * declaration it documents.
109
+ *
110
+ * So: when the comment's PARENT is itself a declaration container, it belongs
111
+ * to that parent; otherwise it belongs to its next named sibling. Both are
112
+ * grammar-structure facts, and both are indexed by the anchor node's start line
113
+ * so attachment stays O(1) with no nearest-previous scan.
114
+ */
115
+ /**
116
+ * ONE traversal that does the work of three: file doc comments, the module
117
+ * surface (import / from-import / include / export), and the file's own
118
+ * cyclomatic complexity.
119
+ *
120
+ * These were three independent full-tree walks. They are three because they
121
+ * were written at three different times, not because they need three visits:
122
+ * all three are pure visitors over the same nodes with no shared state and no
123
+ * order dependency between them. Measured on a 200-file sample they were 5-9%,
124
+ * 8-19% and 4-19% of analyzer time, so the redundant traversal was a third of
125
+ * the budget spent re-reading the same tree.
126
+ *
127
+ * The fused walk keeps ONE plain local counter and pushes onto ONE stack. That
128
+ * is deliberate and it is the lesson from the rejected memoization: the win
129
+ * here is fewer node VISITS (a WASM boundary crossing each), and the moment a
130
+ * fused walk allocates per-node state — a Map keyed by node id, say — it costs
131
+ * more than the walks it replaced. Nothing here allocates per node.
132
+ *
133
+ * Named children only, for the reason the complexity counter needs it: an
134
+ * anonymous keyword token carries the same type string as the statement that
135
+ * owns it, so descending into anonymous children double-counts every branch.
136
+ */
137
+ collectDocModulesAndComplexity(rootNode, out, imports, exports, qualifiers, relationships) {
138
+ const isDeclarationContainer = (n) => !!this.callableNodes[n.type] || n.type === "type_declaration" || n.type === "variable_declaration" || n.type === "enum_field_declaration" || n.type === "field_declaration";
139
+ let complexity = 1;
140
+ const stack = [rootNode];
141
+ while (stack.length > 0) {
142
+ const n = stack.pop();
143
+ const t = n.type;
144
+ if (BRANCH_NODES.indexOf(t) !== -1) complexity++;
145
+ else if (t === "infix_expression") {
146
+ const op = n.childForFieldName("operator");
147
+ if (op && !op.isNamed && (op.type === "and" || op.type === "or" || op.type === "xor")) complexity++;
148
+ }
149
+ if (t === "documentation_comment" || t === "block_documentation_comment") {
150
+ const text = this.docCommentText(n);
151
+ if (text) {
152
+ const parent = n.parent;
153
+ const anchor = parent && isDeclarationContainer(parent) ? parent : n.nextNamedSibling;
154
+ if (anchor) out.set(anchor.startPosition.row + 1, text);
155
+ }
156
+ continue;
157
+ }
158
+ if (t === "import_statement") {
159
+ this.readImportStatement(n, imports, qualifiers, relationships);
160
+ } else if (t === "import_from_statement") {
161
+ this.readImportFromStatement(n, imports, qualifiers, relationships);
162
+ } else if (t === "include_statement") {
163
+ this.readIncludeStatement(n, imports, relationships);
164
+ } else if (t === "export_statement") {
165
+ this.readExportStatement(n, exports, relationships);
166
+ }
167
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
168
+ const c = n.namedChild(i);
169
+ if (c) stack.push(c);
170
+ }
171
+ }
172
+ return complexity;
173
+ }
174
+ /**
175
+ * Strip the `##` / `##[` opener and the comment body indentation by index
176
+ * arithmetic, never by a pattern over the comment text — the opener is the
177
+ * grammar's own anonymous token, so its length is known from the node, and
178
+ * the body is a single `comment_content` child. No regex is involved, so
179
+ * there is nothing here for the regex census to classify.
180
+ */
181
+ docCommentText(node) {
182
+ const opener = node.childCount > 0 ? node.child(0) : null;
183
+ const openerText = opener && !opener.isNamed ? opener.text : "";
184
+ const content = node.namedChildCount > 0 ? node.namedChild(0) : null;
185
+ let raw = content ? content.text : node.text;
186
+ if (!content && openerText && raw.startsWith(openerText)) raw = raw.slice(openerText.length);
187
+ const lines = raw.split("\n");
188
+ const cleaned = [];
189
+ for (const line of lines) {
190
+ let l = line;
191
+ let i = 0;
192
+ while (i < l.length && (l[i] === " " || l[i] === " ")) i++;
193
+ cleaned.push(i > 0 ? l.slice(i) : l);
194
+ }
195
+ while (cleaned.length > 0 && cleaned[0].trim() === "") cleaned.shift();
196
+ while (cleaned.length > 0 && cleaned[cleaned.length - 1].trim() === "") cleaned.pop();
197
+ return cleaned.join("\n");
198
+ }
199
+ // ═══════════════════════════════════════════════════════════════════════
200
+ // 2. SYMBOLS
201
+ // ═══════════════════════════════════════════════════════════════════════
202
+ callableNodes = {
203
+ proc_declaration: { kind: ASTEngine.SymbolKind.Function, callableKind: "proc" },
204
+ func_declaration: { kind: ASTEngine.SymbolKind.Function, callableKind: "func" },
205
+ method_declaration: { kind: ASTEngine.SymbolKind.Method, callableKind: "method" },
206
+ iterator_declaration: { kind: ASTEngine.SymbolKind.Function, callableKind: "iterator" },
207
+ template_declaration: { kind: ASTEngine.SymbolKind.Macro, callableKind: "template" },
208
+ macro_declaration: { kind: ASTEngine.SymbolKind.Macro, callableKind: "macro" },
209
+ converter_declaration: { kind: ASTEngine.SymbolKind.Function, callableKind: "converter" }
210
+ };
211
+ /** Every value-binding section in the grammar, with the kind it declares.
212
+ * `const` → Constant; `let`/`var`/`using` → Variable. */
213
+ valueSectionNodes = {
214
+ const_section: ASTEngine.SymbolKind.Constant,
215
+ let_section: ASTEngine.SymbolKind.Variable,
216
+ var_section: ASTEngine.SymbolKind.Variable,
217
+ using_section: ASTEngine.SymbolKind.Variable
218
+ };
219
+ collectDeclarations(rootNode, ctx) {
220
+ const stack = [{ node: rootNode, inCallable: false, enclosingCallable: "", docExample: false, objectKind: "" }];
221
+ while (stack.length > 0) {
222
+ const { node, inCallable, enclosingCallable, docExample, objectKind } = stack.pop();
223
+ const type = node.type;
224
+ if (type === "ERROR" || node.isMissing) {
225
+ ctx.errors.push({
226
+ message: `Nim syntax error: ${this.snippet(node.text)}`,
227
+ range: this.nodeRange(node),
228
+ severity: "error"
229
+ });
230
+ }
231
+ const callable = this.callableNodes[type];
232
+ if (callable) {
233
+ const sym = this.buildCallableSymbol(node, callable, ctx, inCallable, enclosingCallable);
234
+ if (sym) {
235
+ ctx.symbols.push(sym);
236
+ if (inCallable) ctx.nestedCallables.add(sym.name);
237
+ else ctx.fileCallables.add(sym.name);
238
+ }
239
+ this.pushCallableChildren(node, stack, enclosingCallable);
240
+ continue;
241
+ }
242
+ if (type === "type_declaration") {
243
+ if (!inCallable || docExample) {
244
+ const sym = this.buildTypeSymbol(node, ctx, objectKind);
245
+ if (sym) {
246
+ ctx.symbols.push(sym);
247
+ ctx.fileTypes.add(sym.name);
248
+ for (const g of sym.genericParameters ?? []) ctx.genericParamNames.add(g);
249
+ this.pushTypeMembers(node, sym, ctx);
250
+ }
251
+ }
252
+ this.pushChildren(node, stack, inCallable, enclosingCallable, docExample, objectKind);
253
+ continue;
254
+ }
255
+ const sectionKind = this.valueSectionNodes[type];
256
+ if (sectionKind) {
257
+ for (let i = 0; i < node.namedChildCount; i++) {
258
+ const decl = node.namedChild(i);
259
+ if (decl && decl.type === "variable_declaration") {
260
+ for (const sym of this.buildValueSymbols(decl, sectionKind, type, inCallable || docExample, ctx)) {
261
+ ctx.symbols.push(sym);
262
+ ctx.fileValues.add(sym.name);
263
+ }
264
+ }
265
+ }
266
+ this.pushChildren(node, stack, inCallable, enclosingCallable, docExample, objectKind);
267
+ continue;
268
+ }
269
+ const childObjectKind = type === "ref_type" ? "ref_type" : type === "pointer_type" ? "pointer_type" : type === "distinct_type" || type === "out_type" || type === "var_type" ? type : objectKind;
270
+ this.pushChildren(
271
+ node,
272
+ stack,
273
+ inCallable,
274
+ enclosingCallable,
275
+ docExample,
276
+ childObjectKind,
277
+ inCallable ? this.runnableExamplesArgument(node) : null
278
+ );
279
+ }
280
+ }
281
+ /**
282
+ * `runnableExamples:` — a `##` doc-comment block the Nim COMPILER compiles.
283
+ *
284
+ * ## What the grammar does with it, and why it matters
285
+ *
286
+ * The pinned grammar does not give the block a node type. For
287
+ *
288
+ * proc `of`*[T, S](x: T, y: typedesc[S]): bool =
289
+ * ## Checks if `x` is an instance of `y`.
290
+ * runnableExamples:
291
+ * type
292
+ * Base = ref object of RootObj
293
+ * Sub1 = ref object of Base
294
+ * Sub2 = ref object of Base
295
+ *
296
+ * it emits ONE `documentation_comment` node for the first `##` line, and then
297
+ * parses `runnableExamples:` as ordinary CODE — a `call` whose `function` is the
298
+ * identifier `runnableExamples` and whose `argument_list` holds the whole
299
+ * example body — INSIDE the routine's `body` field. So the example's types are
300
+ * body-locals by tree position, and the C-D1 rule (a `type` section inside a
301
+ * body is an execution artifact) correctly suppressed them.
302
+ *
303
+ * That rule is right for a body and wrong here, because a `runnableExamples`
304
+ * block is not the routine's body: it is a separate compilation unit that
305
+ * `nim doc` type-checks, and the declarations in it are real.
306
+ *
307
+ * ## Measured cost of getting it wrong, and what is left
308
+ *
309
+ * Over 4,555 corpus files, 9 `object_declaration.inherits` occurrences on the
310
+ * parse tree had an owner symbol the analyzer published NOWHERE. This rule took
311
+ * that to 6, and the six that remain are three different things, none of them
312
+ * this rule:
313
+ *
314
+ * - 2 in nimsuggest/tests/fixtures/mclass_macro.nim — `type \`typeName\` of
315
+ * \`baseName\``, a macro that BUILDS an inheritance clause from two
316
+ * backtick parameters. The declarations exist only as macro input.
317
+ * - 1 in tests/macros/tgettypeinst.nim — `_ of Model`, where the declared
318
+ * name is `_` and the analyzer suppresses `_` by design (it is Nim's
319
+ * discard name for a declaration).
320
+ * - 1 each in tests/stdlib/trepr.nim (`TFoo`, under `static:` inside a proc)
321
+ * and tests/template/template_issues.nim (`name`, inside a `template` body)
322
+ * — a `type` section inside a BODY, which is the C-D1 rule and is
323
+ * deliberate. A `runnableExamples` block is a separate compilation unit; a
324
+ * `static:` block or a template body is not.
325
+ *
326
+ * So the honest claim is 9 -> 6, with the residue enumerated. An earlier draft
327
+ * of this comment said the rule emptied the bucket; the measurement says
328
+ * otherwise and the measurement is what is written here.
329
+ *
330
+ * ## What is published, and what is not
331
+ *
332
+ * The block's `type_section` declarations are published — they are type
333
+ * declarations the compiler checks. Its value declarations are NOT: `var base:
334
+ * Base = Sub1()` is a local of the EXAMPLE, exactly like a local of a routine
335
+ * body, and publishing it as a file-scope value would be the same defect in the
336
+ * opposite direction.
337
+ *
338
+ * The trigger is read off the tree as a SHAPE WITH A ROLE — a `call` whose
339
+ * `function` field is the identifier `runnableExamples` — not as a text search,
340
+ * so a comment or a string cannot fake it. It fires only inside a routine body,
341
+ * because outside one `runnableExamples` is an ordinary (if odd) call and its
342
+ * arguments are ordinary expressions.
343
+ */
344
+ runnableExamplesArgument(node) {
345
+ if (node.type !== "call") return null;
346
+ const fn = node.childForFieldName("function") ?? this.namedChildOfType(node, "identifier");
347
+ if (!fn || fn.type !== "identifier") return null;
348
+ if (String(fn.text) !== "runnableExamples") return null;
349
+ const args = this.namedChildOfType(node, "argument_list");
350
+ if (!args) return null;
351
+ for (let i = 0; i < args.namedChildCount; i++) {
352
+ const c = args.namedChild(i);
353
+ if (c && (c.type === "statement_list" || c.type === "block")) return args;
354
+ }
355
+ return null;
356
+ }
357
+ /** The first NAMED child of the given node type, or null. */
358
+ namedChildOfType(node, type) {
359
+ for (let i = 0; i < node.namedChildCount; i++) {
360
+ const c = node.namedChild(i);
361
+ if (c && c.type === type) return c;
362
+ }
363
+ return null;
364
+ }
365
+ buildCallableSymbol(node, callable, ctx, inCallable, enclosingCallable) {
366
+ const nameNode = node.childForFieldName("name");
367
+ if (!nameNode) return null;
368
+ const nameInfo = this.readNameNode(nameNode);
369
+ if (!nameInfo.name || nameInfo.name === "_") return null;
370
+ const parameters = this.readParameters(node);
371
+ const returnNode = node.childForFieldName("return_type");
372
+ const body = node.childForFieldName("body");
373
+ const genericParameters = this.readGenericParameterNames(node);
374
+ for (const g of genericParameters) ctx.genericParamNames.add(g);
375
+ const pragmas = this.readPragmaNames(node);
376
+ const hasPragma = (n) => pragmas.indexOf(n) !== -1;
377
+ const doc = ctx.docCommentByStartLine.get(node.startPosition.row + 1);
378
+ const receiverType = callable.callableKind === "method" ? this.methodReceiverTypeLeaf(node) : void 0;
379
+ const nestedIn = inCallable ? enclosingCallable : "";
380
+ const sym = {
381
+ name: nameInfo.name,
382
+ kind: callable.kind,
383
+ visibility: nestedIn ? "private" : nameInfo.exported ? "public" : "private",
384
+ range: this.nodeRange(node),
385
+ scope: nestedIn || void 0,
386
+ parameters,
387
+ returnType: returnNode ? this.nodeText(returnNode) : void 0,
388
+ genericParameters: genericParameters.length > 0 ? genericParameters : void 0,
389
+ decorators: pragmas.length > 0 ? pragmas : void 0,
390
+ comments: doc ? [doc] : void 0,
391
+ // A METHOD's owner is its receiver TYPE, and `ownerName` is the
392
+ // family-standard field for that — the one the store binds for ownership
393
+ // and the one `buildOverrideCompanion` keys on to index
394
+ // (owner -> declared method names) from previously parsed files.
395
+ //
396
+ // Leaving it unset made NIM methods invisible to every owner-based
397
+ // mechanism in the codebase, not just this one: the companion index came
398
+ // back EMPTY for a file whose base class declared a method, so
399
+ // cross-file override resolution could not work no matter what the
400
+ // override pass did. The receiver was already known, just parked in
401
+ // `metadata.nimReceiverType` where nothing generic reads it. The metadata
402
+ // copy stays — the override labels are built from it, and it is the
403
+ // documented place the receiver is recorded.
404
+ ...receiverType ? { ownerName: receiverType, ownerKind: ASTEngine.SymbolKind.Class } : {},
405
+ // `qualifiedName` is the disambiguating handle the override edges need.
406
+ // `overrides` endpoints are receiver-qualified (`Rect.describe`) because the
407
+ // bare name `describe` cannot address either end — and a consumer that
408
+ // resolves an edge by matching a target against published names has no way
409
+ // to reach `Rect.describe` unless some symbol publishes that spelling.
410
+ // Measured on nim/lib/pure (105 files, 6,638 symbols) with
411
+ // `script/m14/incoming-check.ts`: all 14 `overrides` edges reported as
412
+ // UNRESOLVED targets, every one of them a correct receiver-qualified edge.
413
+ // This is the same field the lua lane fills for the same reason
414
+ // (real-lua-analyzer.ts: `qualifiedName: label`, "the `name` field is the
415
+ // identity; this is the path").
416
+ ...receiverType ? { qualifiedName: `${receiverType}.${nameInfo.name}` } : {},
417
+ // The store binds `brain_symbol.docstring` from this field (see
418
+ // parallel-indexer's insertSymbol binding). Emitting only `comments`
419
+ // produced docs=0 for every nim symbol through the real CLI, so the `##`
420
+ // text is published under BOTH names.
421
+ ...doc ? { docstring: doc } : {},
422
+ complexity: body ? this.nimCyclomaticComplexity(body) : 1,
423
+ metadata: {
424
+ nimCallableKind: callable.callableKind,
425
+ // `isDeclaration` is the DECLARATION vs DEFINITION discriminator: the
426
+ // `routine` production makes `=`+`body` optional, so a body-less
427
+ // `proc f(): int` is a forward declaration. Nim has no separate
428
+ // forward-declaration syntax, so this shape only appears in .nims
429
+ // and macro-generated code — but when it does, an `extern`-like
430
+ // declaration competing on equal footing with the definition that
431
+ // implements it is exactly the zig #612 defect class.
432
+ ...body ? {} : { isDeclaration: true },
433
+ ...nameInfo.exported ? { exported: true } : {},
434
+ paramCount: parameters.length,
435
+ ...genericParameters.length > 0 ? { genericParameterCount: genericParameters.length } : {},
436
+ ...nestedIn ? { nimNestedIn: nestedIn } : {},
437
+ ...callable.callableKind === "method" ? { nimReceiverType: receiverType } : {},
438
+ // The receiver-qualified LABEL, recorded in metadata as well as in
439
+ // `qualifiedName`. Two reasons, both measured:
440
+ // - `pool-worker.ts:870` reads `s.qualifiedName || s.metadata?.fqn ||
441
+ // (s.scope ? …)`, so `metadata.fqn` is the worker\'s own declared
442
+ // fallback channel and this is where the same fact belongs for a
443
+ // consumer that reads metadata.
444
+ // - `script/m14/run-fixtures.ts` cannot assert `qualifiedName`: it is
445
+ // absent from that file\'s CONSTRAINT_KEY_ALLOWLIST, so a fixture row
446
+ // carrying it is reported as an INERT assertion and the
447
+ // `nim/method-qualified-name-dropped` mutant survives. `meta.*` keys
448
+ // ARE assertable, so the same fact is pinned here instead of nowhere.
449
+ // The allowlist gap is reported to the supervisor rather than edited
450
+ // (workstream §8).
451
+ ...receiverType ? { fqn: `${receiverType}.${nameInfo.name}` } : {},
452
+ ...node.childForFieldName("rewrite_pattern") ? { nimRewritePattern: true } : {},
453
+ ...hasPragma("raises") ? { nimRaises: true } : {},
454
+ ...hasPragma("noSideEffect") ? { nimNoSideEffect: true } : {},
455
+ ...hasPragma("inline") ? { nimInline: true } : {},
456
+ ...hasPragma("deprecated") ? { nimDeprecated: true } : {},
457
+ ...hasPragma("base") ? { nimBaseMethod: true } : {},
458
+ ...hasPragma("tailcall") ? { nimTailcall: true } : {},
459
+ ...hasPragma("asm") ? { nimAsm: true } : {},
460
+ nameRange: nameInfo.range
461
+ }
462
+ };
463
+ if (doc) sym.metadata.nimDocumented = true;
464
+ return sym;
465
+ }
466
+ buildTypeSymbol(node, ctx, parentObjectKind) {
467
+ const symDecl = node.namedChildCount > 0 ? node.namedChild(0) : null;
468
+ if (!symDecl || symDecl.type !== "type_symbol_declaration") return null;
469
+ const nameNode = symDecl.childForFieldName("name");
470
+ if (!nameNode) return null;
471
+ const nameInfo = this.readNameNode(nameNode);
472
+ if (!nameInfo.name || nameInfo.name === "_") return null;
473
+ const genericParameters = this.readGenericParameterNames(symDecl);
474
+ const doc = ctx.docCommentByStartLine.get(node.startPosition.row + 1);
475
+ let objectKind = parentObjectKind;
476
+ let inherits = null;
477
+ let objectNode = null;
478
+ for (let i = 0; i < node.namedChildCount; i++) {
479
+ const c = node.namedChild(i);
480
+ if (!c || i === 0) continue;
481
+ if (c.type === "ref_type" || c.type === "pointer_type" || c.type === "out_type" || c.type === "var_type" || c.type === "distinct_type") {
482
+ objectKind = c.type;
483
+ const inner = this.lastNamedChild(c);
484
+ if (inner && inner.type === "object_declaration") objectNode = inner;
485
+ } else if (c.type === "object_declaration") {
486
+ objectNode = c;
487
+ } else if (c.type === "type_expression") {
488
+ const inner = c.namedChildCount > 0 ? c.namedChild(0) : null;
489
+ if (inner) {
490
+ if (inner.type === "object_declaration") objectNode = inner;
491
+ else if (inner.type === "ref_type" || inner.type === "pointer_type" || inner.type === "out_type" || inner.type === "var_type" || inner.type === "distinct_type") {
492
+ objectKind = inner.type;
493
+ const deep = this.lastNamedChild(inner);
494
+ if (deep && deep.type === "object_declaration") objectNode = deep;
495
+ }
496
+ }
497
+ }
498
+ }
499
+ if (objectNode && objectKind === "") objectKind = "object";
500
+ if (objectNode) {
501
+ const inh = objectNode.childForFieldName("inherits");
502
+ if (inh) inherits = this.typeLeafName(inh);
503
+ }
504
+ let kind;
505
+ const rhs = this.typeDefinitionRhs(node);
506
+ if (rhs && rhs.type === "concept_declaration") {
507
+ kind = ASTEngine.SymbolKind.Interface;
508
+ } else if (rhs && rhs.type === "enum_declaration") {
509
+ kind = ASTEngine.SymbolKind.Enum;
510
+ } else if (objectNode) {
511
+ kind = objectKind === "ref_type" ? ASTEngine.SymbolKind.Class : ASTEngine.SymbolKind.Struct;
512
+ } else if (rhs && (rhs.type === "tuple_type" || rhs.type === "tuple_deconstruct_declaration" || rhs.type === "tuple_construction")) {
513
+ kind = ASTEngine.SymbolKind.Struct;
514
+ } else {
515
+ kind = ASTEngine.SymbolKind.Type;
516
+ }
517
+ const refines = rhs && rhs.type === "concept_declaration" ? this.readRefinedConcepts(rhs) : [];
518
+ const sym = {
519
+ name: nameInfo.name,
520
+ kind,
521
+ visibility: nameInfo.exported ? "public" : "private",
522
+ range: this.nodeRange(node),
523
+ parameters: rhs && rhs.type === "concept_declaration" ? this.readConceptParameters(rhs) : void 0,
524
+ genericParameters: genericParameters.length > 0 ? genericParameters : void 0,
525
+ comments: doc ? [doc] : void 0,
526
+ ...doc ? { docstring: doc } : {},
527
+ metadata: {
528
+ nimTypeDefinition: true,
529
+ ...objectKind ? { nimObjectKind: objectKind } : {},
530
+ ...inherits ? { nimInherits: inherits } : {},
531
+ ...refines.length > 0 ? { nimRefines: refines } : {},
532
+ ...rhs ? { nimTypeRhs: this.nodeText(rhs) } : {},
533
+ nameRange: nameInfo.range
534
+ }
535
+ };
536
+ if (doc) sym.metadata.nimDocumented = true;
537
+ return sym;
538
+ }
539
+ /**
540
+ * The contracts a concept refines — `type A = concept x of B` names `B`.
541
+ *
542
+ * The grammar puts the clause in a `refines` field of type `refinement_list`,
543
+ * which is `sep1($.type_expression, ",")`, so `of Number, Container` is two
544
+ * entries and each entry is a `type_expression`. Reading the FIELD rather than
545
+ * the `of` keyword is what makes this immune to the failure the arbiter
546
+ * documents about text probes: `is` inside a concept body is a different
547
+ * construct entirely (it is how a concept states its body is satisfied), and a
548
+ * probe on the word would count those too.
549
+ *
550
+ * Each entry is named by its leaf, because the store resolves an edge by
551
+ * matching the endpoint against a symbol name and the R10 rule requires the
552
+ * spelling the lane itself uses. A qualified refinement keeps the written
553
+ * spelling in `nimRefinesQualified`, which is the same two-part record the f#
554
+ * lane writes for `System.IDisposable`.
555
+ */
556
+ readRefinedConcepts(concept) {
557
+ const list = this.refinementListOf(concept);
558
+ if (!list) return [];
559
+ const out = [];
560
+ for (let i = 0; i < list.namedChildCount; i++) {
561
+ const entry = list.namedChild(i);
562
+ if (!entry) continue;
563
+ const leaf = this.typeConstructorLeaf(entry);
564
+ const written = this.nodeText(entry);
565
+ const name = leaf ? leaf.text : this.lastPathSegment(written);
566
+ if (!name || out.indexOf(name) !== -1) continue;
567
+ out.push(name);
568
+ }
569
+ return out;
570
+ }
571
+ /**
572
+ * A `concept_declaration`'s `refinement_list`, or null when the concept
573
+ * refines nothing.
574
+ *
575
+ * ## The field shape, MEASURED
576
+ *
577
+ * `childForFieldName("refines")` on `type A = concept x of Number, Container`
578
+ * returns a node whose OWN TYPE is `refinement_list`, not a wrapper holding
579
+ * one — `childForFieldName("refinement_list")` on the same node returns null,
580
+ * while `childForFieldName("refines")` returns the two-entry list.
581
+ *
582
+ * Both readers of this clause (`readRefinedConcepts` for `implements`, the
583
+ * `concept_declaration` arm of `collectDeclaredTypeRefAt` for `references`)
584
+ * looked for a CHILD of that type on the field node instead, so both returned
585
+ * null on every file in the corpus and on a fixture that spells the construct
586
+ * correctly. Two readers independently wrong about one grammar fact is the
587
+ * reason the fact now has one function: the next grammar bump changes this.
588
+ */
589
+ refinementListOf(concept) {
590
+ const ref = concept.childForFieldName("refines");
591
+ if (!ref) return null;
592
+ return ref.type === "refinement_list" ? ref : this.firstChildOfType(ref, "refinement_list");
593
+ }
594
+ /** The final `/`- or `.`-separated segment of a written type name. Index
595
+ * arithmetic only — no pattern matching, like every other position reader in
596
+ * this analyzer. */
597
+ lastPathSegment(written) {
598
+ let cut = written.length;
599
+ for (let i = written.length - 1; i >= 0; i--) {
600
+ const ch = written[i];
601
+ if (ch === "/" || ch === ".") {
602
+ cut = i + 1;
603
+ break;
604
+ }
605
+ }
606
+ const seg = written.slice(cut);
607
+ return seg.length > 0 && seg.indexOf(" ") === -1 ? seg : "";
608
+ }
609
+ /** The node AFTER the `=` in a `type_declaration` — the `typeDefValue`. */
610
+ typeDefinitionRhs(node) {
611
+ const seenEq = { value: false };
612
+ for (let i = 0; i < node.childCount; i++) {
613
+ const c = node.child(i);
614
+ if (!c) continue;
615
+ if (!seenEq.value) {
616
+ if (!c.isNamed && c.type === "=") seenEq.value = true;
617
+ continue;
618
+ }
619
+ if (c.isNamed) return this.unwrapTypeExpression(c);
620
+ }
621
+ return null;
622
+ }
623
+ /**
624
+ * See through the `type_expression` wrapper the grammar puts around a type
625
+ * RHS, so the classifier sees the SHAPE rather than the wrapper.
626
+ *
627
+ * Found by mutation, not by inspection (nim/tuple-type-classified-as-a-type
628
+ * SURVIVED the first run, which is the harness reporting that a documented
629
+ * rule — "a tuple type is a struct" — had no test behind it and turned out
630
+ * to be FALSE). The trees for the three shapes are not parallel:
631
+ *
632
+ * type Color = enum … -> enum_declaration (no wrapper)
633
+ * type A = concept x -> concept_declaration (no wrapper)
634
+ * type Pair = tuple[x, y] -> type_expression (WRAPPER)
635
+ * └── tuple_type
636
+ *
637
+ * so the old rule matched `tuple_type` at the top level, never saw it behind
638
+ * the wrapper, and `type Pair = tuple[x, y: int]` fell through to the
639
+ * catch-all and published a bare `type` — losing the struct kind its own
640
+ * documentation promised.
641
+ *
642
+ * ONLY THE SHAPES THE WIRED GRAMMAR ACTUALLY DISTINGUISHES ARE LISTED. Seven
643
+ * branches here used to test `array_type` / `generic_type` / `sequence_type` /
644
+ * `set_type` / `table_type` / `range_type` / `ordinal_type` — names from a
645
+ * different tree-sitter-nim revision. The wired build (`wasm/tree-sitter-nim.wasm`,
646
+ * 377 named node types) declares NONE of them, and the shapes they were meant
647
+ * for are not separate nodes at all: `seq[int]`, `array[3, int]`, `set[Color]`,
648
+ * `Table[string, int]` and `range[0..10]` each parse as
649
+ * `type_expression > bracket_expression` (each spelling parsed against the wired
650
+ * wasm on 2026-11-04 to confirm). The classifier below deliberately publishes
651
+ * every one of those as a type-level name through its catch-all, so the seven
652
+ * branches were unreachable and no behaviour is lost by deleting them. A
653
+ * correct-name rewrite (`bracket_expression`) was considered and rejected as a
654
+ * NO-OP: `typeConstructorLeaf`/`typeArgumentNodes` descend through the wrapper
655
+ * either way and the classifier's catch-all is the same branch, so the rewrite
656
+ * would only add code whose reachability no fixture could distinguish. Dead
657
+ * branches are deleted rather than kept, per the F1 rule.
658
+ */
659
+ unwrapTypeExpression(node) {
660
+ let cur = node;
661
+ for (let i = 0; i < 8; i++) {
662
+ if (cur.type !== "type_expression") return cur;
663
+ const inner = this.firstChildOfType(cur, "tuple_type") ?? this.firstChildOfType(cur, "tuple_construction") ?? this.firstChildOfType(cur, "enum_declaration") ?? this.firstChildOfType(cur, "concept_declaration") ?? this.firstChildOfType(cur, "object_declaration") ?? this.firstChildOfType(cur, "ref_type") ?? this.firstChildOfType(cur, "distinct_type") ?? this.firstChildOfType(cur, "pointer_type");
664
+ if (!inner) return cur;
665
+ cur = inner;
666
+ }
667
+ return cur;
668
+ }
669
+ /**
670
+ * Object/enum members. These ARE symbols: the object owns them, so they get
671
+ * `ownerName`/`ownerKind` and a `contains` edge. A field declaration can name
672
+ * several fields at once (`x, y: int`), so one `field_declaration` yields one
673
+ * symbol per `symbol_declaration` — with the declaration's own range and a
674
+ * per-name `nameRange`.
675
+ */
676
+ pushTypeMembers(node, owner, ctx) {
677
+ const stack = [node];
678
+ while (stack.length > 0) {
679
+ const n = stack.pop();
680
+ if (n.type === "field_declaration" || n.type === "variant_discriminator_declaration") {
681
+ for (const sym of this.buildFieldSymbols(n, owner, ctx.docCommentByStartLine)) {
682
+ ctx.symbols.push(sym);
683
+ ctx.fileFieldNames.add(sym.name);
684
+ }
685
+ } else if (n.type === "enum_field_declaration") {
686
+ for (const sym of this.buildEnumFieldSymbols(n, owner, ctx.docCommentByStartLine)) {
687
+ ctx.symbols.push(sym);
688
+ ctx.fileEnumMembers.add(sym.name);
689
+ }
690
+ } else if (n.type === "object_declaration") {
691
+ const inh = n.childForFieldName("inherits");
692
+ if (inh) {
693
+ const base = this.typeLeafName(inh);
694
+ const set = ctx.declaredSubtypes.get(base) ?? /* @__PURE__ */ new Set();
695
+ set.add(owner.name);
696
+ ctx.declaredSubtypes.set(base, set);
697
+ }
698
+ }
699
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
700
+ const c = n.namedChild(i);
701
+ if (c) stack.push(c);
702
+ }
703
+ }
704
+ }
705
+ /** True when some ancestor of `node` has the given type, within a bounded
706
+ * walk. A malformed tree cannot hang this: the guard is 64 levels, deeper
707
+ * than any declaration nesting the grammar produces. */
708
+ hasAncestorOfType(node, type) {
709
+ let cur = node.parent;
710
+ for (let i = 0; cur && i < 64; i++) {
711
+ if (cur.type === type) return true;
712
+ cur = cur.parent;
713
+ }
714
+ return false;
715
+ }
716
+ /** True when this `variant_discriminator_declaration` holds an author-written
717
+ * discriminator rather than a branch list the parser absorbed.
718
+ *
719
+ * A genuine discriminator is written adjacent to `case` — `case kind: Kind` —
720
+ * so its name node shares a source row with the `case` token. The bare form
721
+ * has no name to write, recovery attaches the following `of A, B:` line to the
722
+ * slot, and that line is on a later row by construction.
723
+ *
724
+ * The `case` token is looked up among the owning `variant_declaration`'s
725
+ * children rather than through a field name, because the grammar exposes it as
726
+ * an anonymous `case` token. A missing token is answered pessimistically
727
+ * (NOT adjacent), so an unexpected tree drops the field instead of publishing a
728
+ * fabricated one — the asymmetry is deliberate: a lost field is recoverable,
729
+ * a fabricated edge poisons `bb_graph`, `bb_impact` and `bb_migrate`. */
730
+ discriminatorIsAdjacentToCase(node) {
731
+ const owner = node.parent;
732
+ if (!owner || owner.type !== "variant_declaration") return false;
733
+ let caseTok = null;
734
+ for (let i = 0; i < owner.childCount; i++) {
735
+ const c = owner.child(i);
736
+ if (c && c.type === "case") {
737
+ caseTok = c;
738
+ break;
739
+ }
740
+ }
741
+ if (!caseTok) return false;
742
+ return caseTok.startPosition.row === node.startPosition.row;
743
+ }
744
+ buildFieldSymbols(node, owner, docCommentByStartLine) {
745
+ const out = [];
746
+ const list = node.namedChildCount > 0 ? node.namedChild(0) : null;
747
+ if (!list || list.type !== "symbol_declaration_list") return out;
748
+ const typeNode = node.childForFieldName("type");
749
+ if (!typeNode && this.hasAncestorOfType(node, "tuple_type")) return out;
750
+ if (node.type === "variant_discriminator_declaration" && !this.discriminatorIsAdjacentToCase(node)) {
751
+ return out;
752
+ }
753
+ const doc = docCommentByStartLine.get(node.startPosition.row + 1);
754
+ for (let i = 0; i < list.namedChildCount; i++) {
755
+ const decl = list.namedChild(i);
756
+ if (!decl) continue;
757
+ if (decl.type === "tuple_deconstruct_declaration") {
758
+ for (const s of this.buildFieldSymbols(decl, owner, docCommentByStartLine)) out.push(s);
759
+ continue;
760
+ }
761
+ if (decl.type !== "symbol_declaration") continue;
762
+ const nameNode = decl.childForFieldName("name");
763
+ if (!nameNode) continue;
764
+ const nameInfo = this.readNameNode(nameNode);
765
+ if (!nameInfo.name || nameInfo.name === "_") continue;
766
+ out.push({
767
+ name: nameInfo.name,
768
+ kind: ASTEngine.SymbolKind.Property,
769
+ visibility: nameInfo.exported ? "public" : "private",
770
+ range: this.nodeRange(node),
771
+ scope: owner.name,
772
+ ownerName: owner.name,
773
+ ownerKind: owner.kind,
774
+ returnType: typeNode ? this.nodeText(typeNode) : void 0,
775
+ comments: doc ? [doc] : void 0,
776
+ ...doc ? { docstring: doc } : {},
777
+ metadata: {
778
+ nimField: true,
779
+ ...node.type === "variant_discriminator_declaration" ? { nimVariantDiscriminator: true } : {},
780
+ nameRange: nameInfo.range
781
+ }
782
+ });
783
+ }
784
+ return out;
785
+ }
786
+ buildEnumFieldSymbols(node, owner, docCommentByStartLine) {
787
+ const out = [];
788
+ const decl = node.namedChildCount > 0 ? node.namedChild(0) : null;
789
+ if (!decl || decl.type !== "symbol_declaration") return out;
790
+ const nameNode = decl.childForFieldName("name");
791
+ if (!nameNode) return out;
792
+ const nameInfo = this.readNameNode(nameNode);
793
+ if (!nameInfo.name || nameInfo.name === "_") return out;
794
+ const doc = docCommentByStartLine.get(node.startPosition.row + 1);
795
+ out.push({
796
+ name: nameInfo.name,
797
+ kind: ASTEngine.SymbolKind.EnumMember,
798
+ visibility: nameInfo.exported ? "public" : "private",
799
+ range: this.nodeRange(node),
800
+ scope: owner.name,
801
+ ownerName: owner.name,
802
+ ownerKind: owner.kind,
803
+ comments: doc ? [doc] : void 0,
804
+ metadata: { nimEnumMember: true, nameRange: nameInfo.range }
805
+ });
806
+ return out;
807
+ }
808
+ /**
809
+ * `const`/`let`/`var`/`using` declarations. Nim allows several names in one
810
+ * declaration (`var a, b: int`); each is a symbol with the declaration's
811
+ * range and its own `nameRange`.
812
+ */
813
+ buildValueSymbols(decl, kind, section, inCallable, ctx) {
814
+ if (inCallable) return [];
815
+ const out = [];
816
+ const list = decl.namedChildCount > 0 ? decl.namedChild(0) : null;
817
+ if (!list || list.type !== "symbol_declaration_list") return out;
818
+ const typeNode = decl.childForFieldName("type");
819
+ const valueNode = decl.childForFieldName("value");
820
+ const doc = ctx.docCommentByStartLine.get(decl.startPosition.row + 1);
821
+ for (let i = 0; i < list.namedChildCount; i++) {
822
+ const d = list.namedChild(i);
823
+ if (!d) continue;
824
+ if (d.type === "tuple_deconstruct_declaration") {
825
+ continue;
826
+ }
827
+ if (d.type !== "symbol_declaration") continue;
828
+ const nameNode = d.childForFieldName("name");
829
+ if (!nameNode) continue;
830
+ const nameInfo = this.readNameNode(nameNode);
831
+ if (!nameInfo.name || nameInfo.name === "_") continue;
832
+ out.push({
833
+ name: nameInfo.name,
834
+ kind,
835
+ visibility: nameInfo.exported ? "public" : "private",
836
+ range: this.nodeRange(decl),
837
+ returnType: typeNode ? this.nodeText(typeNode) : void 0,
838
+ comments: doc ? [doc] : void 0,
839
+ metadata: {
840
+ nimValueBinding: true,
841
+ nimSection: section,
842
+ ...valueNode ? { nimHasInitializer: true } : {},
843
+ ...nameInfo.exported ? { exported: true } : {},
844
+ nameRange: nameInfo.range
845
+ }
846
+ });
847
+ }
848
+ return out;
849
+ }
850
+ /**
851
+ * Names bound by ONE `let`/`var`/`const` head, including destructuring.
852
+ *
853
+ * Measured defect this fixes (pixie tests/megatest_emoji.nim line 42, found by
854
+ * the NIM differential, not by inspection): the frame binder walked a
855
+ * `symbol_declaration_list` and took only children whose type was
856
+ * `symbol_declaration`, so a DESTRUCTURING binding was skipped whole. The
857
+ * grammar nests those:
858
+ *
859
+ * variable_declaration "(_, name, _) = splitFile(filePath)"
860
+ * symbol_declaration_list
861
+ * tuple_deconstruct_declaration
862
+ * symbol_declaration "_" -> blank_identifier
863
+ * symbol_declaration "name" -> identifier
864
+ * symbol_declaration "_" -> blank_identifier
865
+ *
866
+ * `name` therefore never entered the frame, `isShadowed("name")` was false at
867
+ * the `images.add((name, image))` read, and the analyzer attributed a read of
868
+ * the LOCAL to `EmojiSet.name` — a struct property declared 26 lines earlier
869
+ * and unrelated to the binding. The edge was not dangling (the target is a
870
+ * real symbol), so no dangling check could ever have caught it: only the
871
+ * shadow rule was wrong. That is the whole class — a same-named field
872
+ * anywhere in the file could absorb any destructured, iterated or
873
+ * pattern-bound local.
874
+ *
875
+ * Recursive, because the pattern nests: `let ((a, b), c) = …` is a
876
+ * tuple_deconstruct_declaration inside a tuple_deconstruct_declaration.
877
+ */
878
+ readBoundDeclarationNames(list) {
879
+ const out = [];
880
+ const visit = (n) => {
881
+ if (!n) return;
882
+ if (n.type === "tuple_deconstruct_declaration") {
883
+ for (let i = 0; i < n.namedChildCount; i++) visit(n.namedChild(i));
884
+ return;
885
+ }
886
+ if (n.type !== "symbol_declaration") return;
887
+ const nameNode = n.childForFieldName("name");
888
+ const info = nameNode ? this.readNameNode(nameNode) : null;
889
+ if (info?.name && info.name !== "_") out.push(info.name);
890
+ };
891
+ for (let i = 0; i < list.namedChildCount; i++) visit(list.namedChild(i));
892
+ return out;
893
+ }
894
+ readParameters(node) {
895
+ const paramsNode = node.childForFieldName("parameters");
896
+ if (!paramsNode) return [];
897
+ const out = [];
898
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
899
+ const p = paramsNode.namedChild(i);
900
+ if (!p) continue;
901
+ if (p.type === "parameter_declaration") {
902
+ const typeNode = p.childForFieldName("type");
903
+ const valueNode = p.childForFieldName("value");
904
+ const list = p.namedChildCount > 0 ? p.namedChild(0) : null;
905
+ if (!list) continue;
906
+ for (let j = 0; j < list.namedChildCount; j++) {
907
+ const d = list.namedChild(j);
908
+ if (!d || d.type !== "symbol_declaration") continue;
909
+ const nameNode = d.childForFieldName("name");
910
+ if (!nameNode) continue;
911
+ const info = this.readNameNode(nameNode);
912
+ if (!info.name) continue;
913
+ out.push({
914
+ name: info.name,
915
+ type: typeNode ? this.nodeText(typeNode) : void 0,
916
+ defaultValue: valueNode ? this.nodeText(valueNode) : void 0,
917
+ isOptional: valueNode != null,
918
+ ...info.exported ? { exported: true } : {}
919
+ });
920
+ }
921
+ }
922
+ }
923
+ return out;
924
+ }
925
+ readGenericParameterNames(node) {
926
+ const field = node.childForFieldName("generic_parameters");
927
+ if (field) return this.genericNamesFrom(field);
928
+ const direct = this.firstChildOfType(node, "generic_parameter_list");
929
+ if (direct) return this.genericNamesFrom(direct);
930
+ const first = node.namedChildCount > 0 ? node.namedChild(0) : null;
931
+ if (first && first.type === "type_symbol_declaration") return this.readGenericParameterNames(first);
932
+ return [];
933
+ }
934
+ genericNamesFrom(list) {
935
+ const out = [];
936
+ for (let i = 0; i < list.namedChildCount; i++) {
937
+ const p = list.namedChild(i);
938
+ if (!p) continue;
939
+ if (p.type !== "parameter_declaration") {
940
+ if (p.type === "identifier") out.push(p.text);
941
+ continue;
942
+ }
943
+ const declList = p.namedChildCount > 0 ? p.namedChild(0) : null;
944
+ if (!declList) continue;
945
+ for (let j = 0; j < declList.namedChildCount; j++) {
946
+ const d = declList.namedChild(j);
947
+ if (!d) continue;
948
+ if (d.type === "symbol_declaration") {
949
+ const n = d.childForFieldName("name");
950
+ const info = n ? this.readNameNode(n) : null;
951
+ if (info?.name) out.push(info.name);
952
+ } else if (d.type === "tuple_deconstruct_declaration") {
953
+ const info = this.readNameNode(d);
954
+ if (info.name) out.push(info.name);
955
+ }
956
+ }
957
+ }
958
+ return out;
959
+ }
960
+ readConceptParameters(concept) {
961
+ const list = concept.childForFieldName("parameters");
962
+ if (!list) return [];
963
+ const out = [];
964
+ for (let i = 0; i < list.namedChildCount; i++) {
965
+ const p = list.namedChild(i);
966
+ if (!p) continue;
967
+ if (p.type === "identifier") {
968
+ out.push({ name: p.text });
969
+ continue;
970
+ }
971
+ if (p.namedChildCount > 0) {
972
+ const sym = p.namedChild(p.namedChildCount - 1);
973
+ if (sym && sym.type === "identifier") {
974
+ out.push({ name: sym.text, ...p.type === "type_parameter" ? { isType: true } : {} });
975
+ }
976
+ }
977
+ }
978
+ return out;
979
+ }
980
+ /** Pragma names in the declaration's own `pragmas` field. `{.raises: [].}`
981
+ * parses as `colon_expression(left: identifier(raises), …)`, so the pragma
982
+ * NAME is the left side — the right side is the argument, not the pragma. */
983
+ readPragmaNames(node) {
984
+ const list = node.childForFieldName("pragmas") ?? this.firstChildOfType(node, "pragma_list");
985
+ if (!list) return [];
986
+ return this.pragmaNameNodes(list).map((p) => p.name);
987
+ }
988
+ /**
989
+ * THE ONE RULE for reading pragma NAMES out of a `pragma_list`, WITH each name's own line.
990
+ *
991
+ * `{.raises: [].}` parses as `colon_expression(left: identifier(raises), …)`, so the pragma NAME is
992
+ * the left side — the right side is the argument, not the pragma. The rule lives here ONCE because
993
+ * three readers now share it (symbol metadata, the `pragma` template marker, and the `decorates`
994
+ * emission), and the fleet has paid for a rule copied in two places: the ruby lane's operator-name
995
+ * class was extended at one of its two sites and 7 rows stayed dropped while a commit message
996
+ * claimed they were exempt. A COPY OF THIS RULE IS A DEFECT WAITING FOR ITS SECOND FIX.
997
+ *
998
+ * The LINE is the name node's own line rather than the declaration's: the fleet's line rule is
999
+ * "the line of the NAMING node", and for a pragma the naming node is the pragma identifier inside
1000
+ * `{. … .}`. A multi-line pragma list puts its names on later lines than the declaration, and
1001
+ * anchoring to the declaration would point a reader at the wrong line for every such row.
1002
+ */
1003
+ pragmaNameNodes(list) {
1004
+ const out = [];
1005
+ for (let i = 0; i < list.namedChildCount; i++) {
1006
+ let c = list.namedChild(i);
1007
+ if (!c) continue;
1008
+ if (c.type === "colon_expression" || c.type === "equal_expression") c = c.childForFieldName("left") ?? c;
1009
+ if (c && c.type === "identifier") out.push({ name: c.text, line: c.startPosition.row + 1 });
1010
+ }
1011
+ return out;
1012
+ }
1013
+ // ═══════════════════════════════════════════════════════════════════════
1014
+ // 3. MODULES: import / export / include
1015
+ // ═══════════════════════════════════════════════════════════════════════
1016
+ /**
1017
+ * `doc/grammar.txt`:
1018
+ * importStmt = 'import' optInd expr ((comma expr)* / 'except' optInd (expr^+ comma))
1019
+ * exportStmt = 'export' optInd expr ((comma expr)* / 'except' optInd (expr^+ comma))
1020
+ * fromStmt = 'from' expr 'import' optInd expr (comma expr)*
1021
+ * includeStmt= 'include' optInd expr ^+ comma
1022
+ *
1023
+ * The module PATH is not always an identifier. `import std/[os, strutils]`
1024
+ * parses as `infix_expression(left: std, operator: /, right:
1025
+ * array_construction(os, strutils))` — the `/` is the stdlib path separator
1026
+ * and the bracket is a module LIST. `import std/os as o` is the same
1027
+ * infix shape with the `as` operator, where the right side is the local
1028
+ * ALIAS, not a module. `import ./rel` and `import ../parent` are relative
1029
+ * paths in the same infix shape. `import "file.nim"` is a string literal.
1030
+ */
1031
+ /**
1032
+ * `from M import a, b` — the from-form. Extracted from the old collectModules
1033
+ * walk so the fused walk can call it as a handler; the body is unchanged, and
1034
+ * the path reader is the SAME one `import` uses, so a module spelled `./x` in
1035
+ * one statement and `x` in another is still one target.
1036
+ */
1037
+ readImportFromStatement(n, imports, qualifiers, relationships) {
1038
+ const moduleNode = n.childForFieldName("module");
1039
+ const modulePathsParsed = moduleNode ? this.parseModuleExpression(moduleNode) : null;
1040
+ const module = modulePathsParsed?.paths[0]?.path ?? "";
1041
+ const list = this.firstChildOfType(n, "expression_list");
1042
+ const items = this.identifierTextsIn(list);
1043
+ if (module) {
1044
+ imports.push({
1045
+ module,
1046
+ items,
1047
+ isDefaultImport: false,
1048
+ range: this.nodeRange(n),
1049
+ metadata: { nimImport: true, nimFromImport: true }
1050
+ });
1051
+ for (const seg of this.moduleSegments(module)) qualifiers.add(seg);
1052
+ relationships.push({
1053
+ source: "(global)",
1054
+ target: this.moduleNameOfPath(module),
1055
+ type: ASTEngine.RelationshipType.Imports,
1056
+ confidence: 1,
1057
+ line_number: moduleNode ? moduleNode.startPosition.row + 1 : n.startPosition.row + 1,
1058
+ // Only the edge TARGET is normalised, and only so that it can equal the module
1059
+ // symbol's name — the two spellings drifted apart, which is what left 8,404 of
1060
+ // 13,364 `imports` edges with no candidate to bind to. The path AS WRITTEN
1061
+ // survives in `imports[].module` and on `meta.nimModulePath`.
1062
+ metadata: this.moduleEdgeMetadata("nim_import", module)
1063
+ });
1064
+ }
1065
+ }
1066
+ /**
1067
+ * `include a, b, c` — one edge per operand, through the same path reader.
1068
+ *
1069
+ * ## THE EDGE IS `part_of`, NOT `imports` — a 1:1 RELABEL AT RUNG R3 (2026-10-23)
1070
+ *
1071
+ * Nim's `include` is a TEXTUAL scope-sharing inclusion: the included file's declarations become
1072
+ * part of the INCLUDING module's single declaration space; no module is created, no binding
1073
+ * scope is imported, and nothing can `import` what was included. That is exactly the `part_of`
1074
+ * contract (`src/ast/types.ts:164-190`: "A library unit INCLUDES a part file … a part shares
1075
+ * the library's single scope, imports nothing, and cannot be resolved as a separate module").
1076
+ * This file says the same thing in Nim's words further down: "`.nims`. A NimScript file is
1077
+ * `include`d into another module; it is not a module and nothing can `import` it."
1078
+ *
1079
+ * So the stored TYPE moves `imports` -> `part_of` and NOTHING ELSE moves: the same source, the
1080
+ * same target, the same line, the same confidence, the same `relationshipType: "nim_include"`
1081
+ * marker, and the same `ImportInfo` payload pushed above (which `src/ast/parallel-indexer.ts`
1082
+ * persists into `brain_import` — a consumer reading a file's directive list loses nothing when
1083
+ * the relationship's label moves).
1084
+ *
1085
+ * THE FLEET'S PRECEDENT, three lanes and the same relabel discipline:
1086
+ * · fsharp (`real-fsharp-analyzer.ts:1900-1965`): `#load` -> `part_of`, 1:1;
1087
+ * · php (`real-php-analyzer.ts:3085-3100`): include/require -> `part_of`, 1:1;
1088
+ * · dart (`real-dart-analyzer.ts`): `part`/`part of` -> `part_of`.
1089
+ *
1090
+ * ## THE DIVERGENCE FROM fsharp/php IS RECORDED, NOT FLATTENED
1091
+ *
1092
+ * fsharp and php emit a PROJECT-RELATIVE PATH as the target (`targetResolution:
1093
+ * "project-relative-path"`). Nim's target STAYS the analyzer's normalized MODULE NAME — the
1094
+ * artifact of the 2026-10-14 import-resolution work, measured at 96.9 % intra-file / 98.0 %
1095
+ * corpus-wide binding for the 458 `nim_include` edges (`validation/m14/fixtures/nim/
1096
+ * import-resolution-probe.mts`, `s18-import-probe.txt`), and reverting the normalization to the
1097
+ * verbatim path binds 0 of the 14 it leaves unresolved. The target rule is a property of the
1098
+ * construct — a Nim include names a module path expression, not a string file literal — and a
1099
+ * reader comparing two lanes' `part_of` cells must be able to see that they are resolved in
1100
+ * DIFFERENT KEYSPACES. Hence `targetResolution: "module-name"` on every row: the shape is
1101
+ * named on the row rather than normalized away.
1102
+ *
1103
+ * ## DIRECTION AND `partRole`
1104
+ *
1105
+ * The direction is the directive's own (types.ts:170-183): the source is the file carrying the
1106
+ * directive (`(global)`, this analyzer's file-level source convention), the target is what it
1107
+ * names. Nim has only ONE of Dart's two roles — there is no `part of` counterpart, because an
1108
+ * included file does not know it is included — so every row is the host that INCLUDES the
1109
+ * part: `partRole: "library"`, the same single-role shape php declares.
1110
+ */
1111
+ readIncludeStatement(n, imports, relationships) {
1112
+ const list = this.firstChildOfType(n, "expression_list");
1113
+ const includePaths = [];
1114
+ if (list) {
1115
+ for (let i = 0; i < list.namedChildCount; i++) {
1116
+ const c = list.namedChild(i);
1117
+ if (!c) continue;
1118
+ const parsedInc = this.parseModuleExpression(c);
1119
+ if (parsedInc) for (const pth of parsedInc.paths) includePaths.push(pth.path);
1120
+ }
1121
+ }
1122
+ for (const text of includePaths) {
1123
+ imports.push({
1124
+ module: text,
1125
+ isDefaultImport: false,
1126
+ range: this.nodeRange(n),
1127
+ metadata: { nimInclude: true }
1128
+ });
1129
+ relationships.push({
1130
+ source: "(global)",
1131
+ target: this.moduleNameOfPath(text),
1132
+ type: ASTEngine.RelationshipType.PartOf,
1133
+ confidence: 1,
1134
+ line_number: n.startPosition.row + 1,
1135
+ metadata: this.moduleEdgeMetadata("nim_include", text, {
1136
+ // MANDATORY on every `part_of` row (src/ast/types.ts:170-183): the direction is the
1137
+ // directive's own and `partRole` says which of Dart's two roles this row is. Nim has
1138
+ // only the host role.
1139
+ partRole: "library",
1140
+ // THE RECORDED DIVERGENCE: fsharp and php emit project-relative PATHS; nim keeps the
1141
+ // analyzer's normalized MODULE NAME (see this function's header for the measurement).
1142
+ targetResolution: "module-name"
1143
+ })
1144
+ });
1145
+ }
1146
+ }
1147
+ /**
1148
+ * `export a, b` — the module's RE-EXPORT, and the only Nim export fact that
1149
+ * has no other carrier in this codebase.
1150
+ *
1151
+ * ## Why this emits an `exports` edge when the `*` marker does not
1152
+ *
1153
+ * Nim has two ways to make a name visible outside its module and they are
1154
+ * different facts, so they are not interchangeable:
1155
+ *
1156
+ * - the `*` marker (`proc foo*()`) sets the VISIBILITY of a declaration.
1157
+ * This analyzer already publishes that, as `visibility: "public"` plus
1158
+ * `metadata.exported`, and the parallel indexer persists it into
1159
+ * `brain_symbol.visibility`, where it already drives `gravity_score`
1160
+ * (parallel-indexer.ts:3044, :3052). Measured over the corpus: 28,900
1161
+ * `exported_symbol` nodes in 1,335 files. Publishing a second 28,900-edge
1162
+ * graph saying the same thing would grow the relationship payload by
1163
+ * ~17.6% (163,960 edges measured before) to add nothing a query could not
1164
+ * already ask. It is therefore NOT a gap, and the number is recorded here
1165
+ * so a reader can disagree with the judgement using the measurement rather
1166
+ * than the prose.
1167
+ *
1168
+ * - the `export` STATEMENT re-publishes names this module IMPORTED. Nothing
1169
+ * on any symbol records that, no metadata field carries it, and the LSP
1170
+ * usages file-level section — which answers with
1171
+ * `relationship_type IN ('imports','exports')`, the one reader that answers
1172
+ * "what does this module depend on and hand on" — saw nothing for Nim.
1173
+ * That is a capability that emits NOTHING, which is the class every
1174
+ * existing gate is blind to. It is emitted here.
1175
+ *
1176
+ * ## The `except` clause, which the first version got backwards
1177
+ *
1178
+ * `doc/grammar.txt` spells the statement
1179
+ * `exportStmt = 'export' optInd expr ((comma expr)* / 'except' optInd (expr^+ comma))`
1180
+ * and the grammar agrees: `export nifpools except info, NoLineInfo, tagId, …`
1181
+ * parses as `export_statement(identifier(nifpools), except_clause(expression_list(…)))`.
1182
+ *
1183
+ * The old reader walked every identifier under the statement, so it listed the
1184
+ * EXCEPTED names as exported — the precise inverse of what the construct means,
1185
+ * and a mistake that inflates the export set with names the author explicitly
1186
+ * refused to re-publish. Measured on the corpus: 9 of 155 `export_statement`
1187
+ * nodes carry an `except_clause`, and they are the whole reason a
1188
+ * name-collecting walk is the wrong reader here. The operands are now read
1189
+ * positionally: an operand inside `except_clause` is an exclusion, it is not
1190
+ * published, and it rides the published operand's `nimExportExcluded`
1191
+ * metadata.
1192
+ *
1193
+ * ## Spelling
1194
+ *
1195
+ * A re-export is usually written qualified — `export astyaml.treeToYaml`,
1196
+ * `export common_parser.Parser` — and R10 requires the endpoint to be spelled
1197
+ * the way the lane spells its own symbols, so the edge carries the LEAF and the
1198
+ * written form is kept in `nimQualifiedName`. This is the same two-part record
1199
+ * the f# lane writes for `System.IDisposable`. A `/`-separated operand is a
1200
+ * MODULE rather than a symbol and is published whole, which is what the
1201
+ * `imports` reader does with the same spelling, so the two spellings of one
1202
+ * module are one target in the graph.
1203
+ *
1204
+ * Confidence 1.0: the statement names the operand outright, with no inference
1205
+ * and no name matching.
1206
+ */
1207
+ readExportStatement(n, exports, relationships) {
1208
+ const exceptClause = this.firstChildOfType(n, "except_clause");
1209
+ const exceptTexts = exceptClause ? this.identifierTextsIn(this.firstChildOfType(exceptClause, "expression_list")) : [];
1210
+ for (const operand of this.exportOperands(n)) {
1211
+ const read = this.readExportedName(operand);
1212
+ if (!read.name) continue;
1213
+ exports.push({
1214
+ name: read.name,
1215
+ metadata: {
1216
+ nimExport: true,
1217
+ nimReExport: true,
1218
+ ...read.qualified ? { nimQualifiedName: read.qualified } : {},
1219
+ ...read.alias ? { nimAlias: read.alias } : {}
1220
+ }
1221
+ });
1222
+ relationships.push({
1223
+ source: "(global)",
1224
+ target: read.name,
1225
+ type: ASTEngine.RelationshipType.Exports,
1226
+ confidence: 1,
1227
+ line_number: operand.startPosition.row + 1,
1228
+ metadata: {
1229
+ relationshipType: "nim_reexport",
1230
+ nimReExport: true,
1231
+ ...read.qualified ? { nimQualifiedName: read.qualified } : {},
1232
+ ...read.alias ? { nimAlias: read.alias } : {},
1233
+ ...read.isModule ? { nimModuleReExport: true } : {},
1234
+ ...exceptTexts.length > 0 ? { nimExportExcluded: exceptTexts } : {}
1235
+ }
1236
+ });
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Drop from the importable-unit list the re-exported names THIS FILE DOES NOT
1241
+ * PROVIDE, keeping them on the `exports` relationship.
1242
+ *
1243
+ * ## Why the list and the edge answer different questions
1244
+ *
1245
+ * `result.exports` is the index's importable-unit set: a name an importer can
1246
+ * bind, with this file as its provider. `exports foo` where `foo` is declared
1247
+ * in another module is the opposite of that — the name is bindable, but
1248
+ * SOMEBODY ELSE provides it, and recording it here would give one name two
1249
+ * providers in the graph. The `exports` RELATIONSHIP is the right carrier for
1250
+ * it, because the fact it states is "this module hands this name onward",
1251
+ * which is true whoever declares it — and it is exactly what the LSP usages
1252
+ * file-level section asks for with
1253
+ * `relationship_type IN ('imports','exports')`.
1254
+ *
1255
+ * This is also the M49 exports-family invariant's own reading, which admits
1256
+ * only names that resolve to a top-level symbol ("membership rides Contains")
1257
+ * and lists the per-language exceptions explicitly. Nim's cross-module
1258
+ * re-export needed no exception once the two carriers were separated; before
1259
+ * this pass every `export` operand landed in the list, so a Nim fixture with
1260
+ * an `export` statement at all failed the invariant on names that were never
1261
+ * claimed to be local.
1262
+ *
1263
+ * The M49 invariant also requires export names to be UNIQUE — a duplicate
1264
+ * cannot identify its provider — and `export a, a` is legal Nim, so the
1265
+ * dedupe is here rather than left to a consumer to discover.
1266
+ *
1267
+ * Runs after `collectDeclarations` because answering "does this file declare
1268
+ * it" needs the symbol set; the readers that fill the list run earlier, in
1269
+ * the module-surface pass.
1270
+ */
1271
+ pruneForeignReExports(symbols, exports) {
1272
+ if (exports.length === 0) return;
1273
+ const local = /* @__PURE__ */ new Set();
1274
+ for (const s of symbols) local.add(s.name);
1275
+ const kept = [];
1276
+ const seen = /* @__PURE__ */ new Set();
1277
+ for (const e of exports) {
1278
+ const isReExport = !!e.metadata?.nimReExport;
1279
+ if (isReExport && !local.has(e.name)) continue;
1280
+ if (seen.has(e.name)) continue;
1281
+ seen.add(e.name);
1282
+ kept.push(e);
1283
+ }
1284
+ exports.length = 0;
1285
+ for (const e of kept) exports.push(e);
1286
+ }
1287
+ /** The `export` statement's operands, positionally: a bare named child, or each
1288
+ * child of its `expression_list`, and never a node inside `except_clause`. */
1289
+ exportOperands(n) {
1290
+ const out = [];
1291
+ for (let i = 0; i < n.namedChildCount; i++) {
1292
+ const c = n.namedChild(i);
1293
+ if (!c || c.type === "except_clause") continue;
1294
+ if (c.type === "expression_list") {
1295
+ for (let j = 0; j < c.namedChildCount; j++) {
1296
+ const item = c.namedChild(j);
1297
+ if (item) out.push(item);
1298
+ }
1299
+ continue;
1300
+ }
1301
+ out.push(c);
1302
+ }
1303
+ return out;
1304
+ }
1305
+ /** One `export` operand, read for the NAME it publishes. A `/` path is a
1306
+ * module and is published whole; a `.` qualification is a symbol reached
1307
+ * through a module and is published by its leaf; `X as Y` renames locally and
1308
+ * still publishes `X`. */
1309
+ readExportedName(node) {
1310
+ if (node.type === "identifier" || node.type === "accent_quoted") return { name: node.text };
1311
+ if (node.type === "infix_expression") {
1312
+ const op = node.childForFieldName("operator");
1313
+ const left = node.childForFieldName("left");
1314
+ const right = node.childForFieldName("right");
1315
+ if (!op || !left) return { name: "" };
1316
+ if (op.type === "as") {
1317
+ const base = this.readExportedName(left);
1318
+ const alias = right && right.type === "identifier" ? right.text : void 0;
1319
+ return alias ? { ...base, alias } : base;
1320
+ }
1321
+ if (op.text.indexOf("/") !== -1) return { name: this.nodeText(node), isModule: true };
1322
+ return { name: "" };
1323
+ }
1324
+ if (node.type === "dot_expression") {
1325
+ const right = node.childForFieldName("right");
1326
+ if (!right || right.type !== "identifier" && right.type !== "accent_quoted") return { name: "" };
1327
+ const leaf = right.text;
1328
+ const written = this.nodeText(node);
1329
+ return written !== leaf ? { name: leaf, qualified: written } : { name: leaf };
1330
+ }
1331
+ return { name: "" };
1332
+ }
1333
+ readImportStatement(node, imports, qualifiers, relationships) {
1334
+ const exceptClause = this.firstChildOfType(node, "except_clause");
1335
+ const exceptTexts = exceptClause ? this.identifierTextsIn(this.firstChildOfType(exceptClause, "expression_list")) : [];
1336
+ const moduleNodes = [];
1337
+ const list = this.firstChildOfType(node, "expression_list");
1338
+ if (list) {
1339
+ for (let i = 0; i < list.namedChildCount; i++) {
1340
+ const c = list.namedChild(i);
1341
+ if (!c) continue;
1342
+ if (exceptClause && this.isDescendantOf(c, exceptClause)) continue;
1343
+ moduleNodes.push(c);
1344
+ }
1345
+ }
1346
+ for (const mod of moduleNodes) {
1347
+ const parsed = this.parseModuleExpression(mod);
1348
+ if (!parsed) continue;
1349
+ for (const { path, alias, qualifier, site } of parsed.paths) {
1350
+ imports.push({
1351
+ module: path,
1352
+ isDefaultImport: false,
1353
+ ...alias ? { alias } : {},
1354
+ ...exceptTexts.length > 0 ? { items: exceptTexts } : {},
1355
+ range: this.nodeRange(node),
1356
+ metadata: { nimImport: true, ...parsed.isPathList ? { nimModuleList: true } : {} }
1357
+ });
1358
+ for (const seg of this.moduleSegments(path)) qualifiers.add(seg);
1359
+ relationships.push({
1360
+ source: "(global)",
1361
+ target: this.moduleNameOfPath(path),
1362
+ type: ASTEngine.RelationshipType.Imports,
1363
+ confidence: 1,
1364
+ line_number: site.startPosition.row + 1,
1365
+ metadata: this.moduleEdgeMetadata("nim_import", path, {
1366
+ ...parsed.isPathList ? { nimModuleList: true } : {},
1367
+ ...alias ? { nimAlias: alias } : {},
1368
+ ...qualifier ? { nimQualifier: qualifier } : {}
1369
+ })
1370
+ });
1371
+ }
1372
+ }
1373
+ }
1374
+ /**
1375
+ * Flatten one `import` operand into module paths.
1376
+ * `strutils` → ["strutils"]
1377
+ * `std/[os, strutils]` → ["std/os", "std/strutils"]
1378
+ * `os, strutils` → two operands at the list level
1379
+ * `std/os as o` → ["std/os"] alias "o"
1380
+ * `./rel`, `../parent` → the relative path, verbatim
1381
+ * `"file.nim"` → the string literal, verbatim
1382
+ */
1383
+ parseModuleExpression(node) {
1384
+ if (node.type === "identifier" || node.type === "accent_quoted") {
1385
+ return { paths: [{ path: node.text, site: node }], isPathList: false };
1386
+ }
1387
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal" || node.type === "long_string_literal") {
1388
+ return { paths: [{ path: this.unquote(node.text), site: node }], isPathList: false };
1389
+ }
1390
+ if (node.type === "array_construction") {
1391
+ const out = [];
1392
+ for (let i = 0; i < node.namedChildCount; i++) {
1393
+ const c = node.namedChild(i);
1394
+ if (!c) continue;
1395
+ const inner = this.parseModuleExpression(c);
1396
+ if (inner) out.push(...inner.paths);
1397
+ }
1398
+ return out.length > 0 ? { paths: out, isPathList: true } : null;
1399
+ }
1400
+ if (node.type === "infix_expression") {
1401
+ const op = node.childForFieldName("operator");
1402
+ const left = node.childForFieldName("left");
1403
+ const right = node.childForFieldName("right");
1404
+ if (!op || !left || !right) return null;
1405
+ if (op.type === "as") {
1406
+ const base = this.parseModuleExpression(left);
1407
+ const alias = right.type === "identifier" ? right.text : void 0;
1408
+ if (!base) return null;
1409
+ return {
1410
+ paths: base.paths.map((p) => ({ path: p.path, ...alias ? { alias } : {}, site: p.site })),
1411
+ isPathList: base.isPathList
1412
+ };
1413
+ }
1414
+ const opText = op.text;
1415
+ if (opText.indexOf("/") === -1) return null;
1416
+ if (right.type === "pragma_expression") {
1417
+ const inner = right.childForFieldName("left");
1418
+ if (!inner || inner.type !== "identifier" && inner.type !== "accent_quoted") return null;
1419
+ const qualifierList = right.childForFieldName("right");
1420
+ return {
1421
+ paths: [{
1422
+ path: `${this.nodeText(left)}/${inner.text}`,
1423
+ site: right,
1424
+ ...qualifierList ? { qualifier: this.nodeText(qualifierList) } : {}
1425
+ }],
1426
+ isPathList: false
1427
+ };
1428
+ }
1429
+ if (right.type === "array_construction") {
1430
+ const out = [];
1431
+ const prefix = this.nodeText(left);
1432
+ for (let i = 0; i < right.namedChildCount; i++) {
1433
+ const c = right.namedChild(i);
1434
+ if (!c) continue;
1435
+ const name = c.type === "identifier" ? c.text : null;
1436
+ if (name) out.push({ path: `${prefix}/${name}`, site: c });
1437
+ }
1438
+ return out.length > 0 ? { paths: out, isPathList: true } : null;
1439
+ }
1440
+ return { paths: [{ path: this.nodeText(node), site: node }], isPathList: false };
1441
+ }
1442
+ if (node.type === "prefix_expression") {
1443
+ const qualOp = node.childForFieldName("operator");
1444
+ if (qualOp) {
1445
+ let operand = null;
1446
+ for (let i = 0; i < node.namedChildCount; i++) {
1447
+ const c = node.namedChild(i);
1448
+ if (c && c.id !== qualOp.id) {
1449
+ operand = c;
1450
+ break;
1451
+ }
1452
+ }
1453
+ if (operand && operand.type === "pragma_expression") {
1454
+ const inner = operand.childForFieldName("left");
1455
+ if (inner && (inner.type === "identifier" || inner.type === "accent_quoted")) {
1456
+ const qualifierList = operand.childForFieldName("right");
1457
+ return {
1458
+ paths: [{
1459
+ path: `${qualOp.text}${inner.text}`,
1460
+ site: operand,
1461
+ ...qualifierList ? { qualifier: this.nodeText(qualifierList) } : {}
1462
+ }],
1463
+ isPathList: false
1464
+ };
1465
+ }
1466
+ }
1467
+ }
1468
+ return { paths: [{ path: this.nodeText(node), site: node }], isPathList: false };
1469
+ }
1470
+ if (node.type === "dot_expression") {
1471
+ return { paths: [{ path: this.nodeText(node), site: node }], isPathList: false };
1472
+ }
1473
+ return null;
1474
+ }
1475
+ /**
1476
+ * The MODULE this file defines, as one `SymbolKind.Module` symbol.
1477
+ *
1478
+ * ## Why this is a symbol and not a scope name
1479
+ *
1480
+ * A Nim `.nim` file IS a module, it has a name, it is the unit `import`
1481
+ * names, and it is the unit `export` publishes from. Every one of those is a
1482
+ * fact about a declaration, and before this pass the graph had no node for it:
1483
+ * every `imports` edge named a module that existed nowhere in the symbol set.
1484
+ *
1485
+ * MEASURED, 6,063 corpus files, production `parseFile` (`census.ts`, artifacts in
1486
+ * `validation/m14/measurements/nim/`): `imports` resolved its target on **37.1%** of
1487
+ * 13,364 edges, and the unresolved population is the module path as written —
1488
+ * `std/assertions`(823) `std/syncio`(671) `std/strutils`(386) `std/os`(297)
1489
+ * `std/tables`(240). 8,404 of 13,364 edges named a module the graph could not bind
1490
+ * to. It was not a resolution failure: there was no candidate to fail to find.
1491
+ *
1492
+ * The store cannot rescue it either, and that is checked rather than assumed. Its
1493
+ * insert-time target resolver matches `brain_symbol.name` (`parallel-indexer.ts:2239`)
1494
+ * and its three post-pass strategies match a bare `name`, a `qualified_name`, or —
1495
+ * for a DOT target — everything after the first `.` (`parallel-indexer.ts:3755-3774`).
1496
+ * `std/assertions` is none of those, and the fourth strategy, `resolveImportTargets`,
1497
+ * is scoped to the `<module>:<item>` form that javascript/typescript/python emit —
1498
+ * nim writes a bare path, so it never reaches that planner.
1499
+ *
1500
+ * ## Why the NAME is the last path segment, and why that is the module
1501
+ *
1502
+ * Nim resolves `import std/assertions` to the module `assertions` in
1503
+ * `lib/std/assertions.nim`; the `std/` prefix is a directory on the module search
1504
+ * path, not part of the module's name. So the symbol is named after the file's
1505
+ * basename and the edge target is normalised to the same rule, which is what makes
1506
+ * the two strings equal. This is the same "publish the string you target" shape the
1507
+ * graphql lane measured at a 75% reduction, applied to the construct nim actually has.
1508
+ *
1509
+ * ## What is deliberately NOT published
1510
+ *
1511
+ * - `.nims`. A NimScript file is `include`d into another module; it is not a module
1512
+ * and nothing can `import` it. Publishing one would be a node the language cannot
1513
+ * name. So `include`'s edge target is normalised by the SAME rule, and that is
1514
+ * measured rather than assumed: 458 `nim_include` edges over the corpus resolve
1515
+ * at both ends on 96.9% in-file and 98.0% in-corpus, and reverting them to the
1516
+ * verbatim path would bind **0** of the 14 they currently leave unresolved
1517
+ * (`import-resolution-probe.mts`, `s18-import-probe.txt`). An earlier draft of
1518
+ * this comment asserted the opposite — that `include` keeps the written path —
1519
+ * while the code normalised it; the measurement decided which of the two was
1520
+ * true and the comment was corrected to match.
1521
+ * - A renamed module. `module Foo` inside `bar.nim` declares module `Foo`, and the
1522
+ * pinned grammar has NO node for it — `grammars/tree-sitter-nim/grammar.js`
1523
+ * mentions "module" twice and neither is a declaration, so there is nothing on
1524
+ * the tree to read. The symbol is therefore named after the FILE. This is a real
1525
+ * inaccuracy and it is bounded: the corpus measurement is in the doc, and a file
1526
+ * that renames its module gets a module symbol no `import` will ever name.
1527
+ * - An empty basename. A path that yields no name publishes nothing rather than an
1528
+ * empty-named symbol.
1529
+ *
1530
+ * The range is the whole `source_file` node, so the symbol covers what it contains.
1531
+ * A file whose root is not a `source_file` falls back to line 1, never to a line 0 —
1532
+ * a line-0 symbol reads as a missed line to every consumer that checks.
1533
+ */
1534
+ publishModuleSymbol(rootNode, filePath, symbols) {
1535
+ const name = this.moduleNameOfPath(filePath);
1536
+ if (!name) return;
1537
+ const lower = filePath.toLowerCase();
1538
+ if (lower.endsWith(".nims")) return;
1539
+ const src = rootNode.type === "source_file" ? rootNode : null;
1540
+ symbols.push({
1541
+ name,
1542
+ kind: ASTEngine.SymbolKind.Module,
1543
+ visibility: "public",
1544
+ range: src ? this.nodeRange(src) : { startLine: 1, startColumn: 0, endLine: 1, endColumn: 0, startIndex: 0, endIndex: 0 },
1545
+ // The module name is the whole identity here, so `qualifiedName` carries it too:
1546
+ // the store's second resolution strategy matches `qualified_name`, and a row that
1547
+ // is correct under one spelling should not be unreachable because it took another.
1548
+ qualifiedName: name,
1549
+ metadata: { nimModule: true, nimModuleName: name }
1550
+ });
1551
+ }
1552
+ /**
1553
+ * The module name a file path denotes: its basename without the extension, which is
1554
+ * Nim's own rule for the name a file has unless it renames the module inside.
1555
+ *
1556
+ * It is also the LAST SEGMENT of an import path, and both spellings go through here so
1557
+ * the publisher and the emitter cannot drift apart — the whole defect this pass exists
1558
+ * to fix is two pieces of code spelling one symbol differently.
1559
+ */
1560
+ moduleNameOfPath(filePath) {
1561
+ const seg = String(filePath ?? "").split("/").pop() ?? "";
1562
+ let end = seg.length;
1563
+ while (end > 0 && seg.charCodeAt(end - 1) === BACKSLASH) end--;
1564
+ const base = seg.slice(0, end);
1565
+ const dot = base.lastIndexOf(".");
1566
+ const stem = dot > 0 ? base.slice(0, dot) : base;
1567
+ const parts = stem.split("/");
1568
+ const last = (parts[parts.length - 1] ?? "").trim();
1569
+ return last;
1570
+ }
1571
+ /**
1572
+ * Is this written module path Nim's own STANDARD LIBRARY?
1573
+ *
1574
+ * ## What the marker is for, and the number that sizes it
1575
+ *
1576
+ * After the module-symbol pass the residual `imports` population that is declared
1577
+ * NOWHERE in the corpus — the population cause C is about — is 485 edges: 476 on
1578
+ * `nim_import` and 9 on `nim_include`. Measured by
1579
+ * `validation/m14/fixtures/nim/import-resolution-probe.mts` over the same 6,063
1580
+ * files, same analyzer digest (`s18-import-probe.txt`):
1581
+ *
1582
+ * bare name 309 (301 import + 8 include)
1583
+ * other path 103 (102 + 1)
1584
+ * std/ path prefix 66 (66 + 0)
1585
+ * parent-relative 8 (7 + 1)
1586
+ *
1587
+ * 66 of 485 — 13.6% — is identifiable from the spelling alone, and this is that
1588
+ * identification. A `std/`-prefixed path is Nim's standard-library root by the
1589
+ * language's own rule, so `nimStdlibModule` is a fact about the PATH, not a guess
1590
+ * about where the declaration lives.
1591
+ *
1592
+ * ## Why the other 419 are NOT marked, which is the honest part
1593
+ *
1594
+ * 309 are BARE names. Per file, `import re` is three indistinguishable things: a
1595
+ * module in this project, a module in `lib/pure` (which is how Nim spells `re`,
1596
+ * `parseutils`, `algorithm`, `math` — no path prefix at all), or a typo. Nothing in
1597
+ * one file separates them, and marking them external would assert that a local module
1598
+ * is not local — the fabricated-edge failure the workstream brief calls the most
1599
+ * expensive mistake available. 103 are paths with some other prefix (a `nimble`
1600
+ * package name, a sibling directory) and 8 are `../`, which is local by definition.
1601
+ *
1602
+ * Separating the bare cases needs whole-program import resolution: for each bare
1603
+ * target, find the file this project declares by that name and ask whether its path
1604
+ * lies under the Nim library roots. That is shared machinery (workstream §8), so the
1605
+ * need is reported rather than built.
1606
+ *
1607
+ * ## Why the key is `nimStdlibModule` and not the family's `isExternalTarget`
1608
+ *
1609
+ * 15 other analyzers write `isExternalTarget`, and in every one of them it means "the
1610
+ * target is NOT declared in this file". For an `imports` edge that is vacuously true
1611
+ * of every edge — a module is never declared in the file that imports it — so the
1612
+ * family key would carry no information here and would be read as if it did. The key
1613
+ * says what is actually known: the path names the standard library.
1614
+ */
1615
+ isStdlibModulePath(writtenPath) {
1616
+ const s = String(writtenPath ?? "");
1617
+ let i = 0;
1618
+ const skipSpace = () => {
1619
+ while (i < s.length && isPathSpace(s.charCodeAt(i))) i++;
1620
+ };
1621
+ skipSpace();
1622
+ if (i < s.length && s.charCodeAt(i) === DOUBLE_QUOTE) i++;
1623
+ skipSpace();
1624
+ if (i + 3 > s.length) return false;
1625
+ if (!startsWithStdAt(s, i)) return false;
1626
+ i += 3;
1627
+ skipSpace();
1628
+ return i < s.length && s.charCodeAt(i) === SLASH;
1629
+ }
1630
+ /**
1631
+ * The metadata block every `imports` edge carries, built from ONE argument so the
1632
+ * written path and the stdlib marker can never be derived from different strings.
1633
+ *
1634
+ * This is the same one-function discipline `moduleNameOfPath` exists for, applied to
1635
+ * the metadata: the defect being paid down is two pieces of code spelling one module
1636
+ * differently, and a marker computed from a different string than the path it
1637
+ * describes is that defect a second time.
1638
+ */
1639
+ moduleEdgeMetadata(relationshipType, writtenPath, extra) {
1640
+ return {
1641
+ relationshipType,
1642
+ isImport: true,
1643
+ fileLevel: true,
1644
+ // The path AS WRITTEN. `brain_import` and the file-dependency engine read this,
1645
+ // and so does anyone asking which file on disk is meant.
1646
+ nimModulePath: writtenPath,
1647
+ ...this.isStdlibModulePath(writtenPath) ? { nimStdlibModule: true } : {},
1648
+ ...extra ?? {}
1649
+ };
1650
+ }
1651
+ // ═══════════════════════════════════════════════════════════════════════
1652
+ // 4. RELATIONSHIPS
1653
+ // ═══════════════════════════════════════════════════════════════════════
1654
+ /** `contains` — owner → owned symbol, at the member's own declaration line. */
1655
+ collectContainment(symbols, relationships) {
1656
+ const seen = /* @__PURE__ */ new Set();
1657
+ for (const s of symbols) {
1658
+ if (s.ownerName) {
1659
+ const key2 = `${s.ownerName} ${s.name}`;
1660
+ if (seen.has(key2)) continue;
1661
+ seen.add(key2);
1662
+ relationships.push({
1663
+ source: s.ownerName,
1664
+ target: s.name,
1665
+ type: ASTEngine.RelationshipType.Contains,
1666
+ confidence: 1,
1667
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
1668
+ metadata: { relationshipType: "nim_contains" }
1669
+ });
1670
+ continue;
1671
+ }
1672
+ const parent = typeof s.metadata?.nimNestedIn === "string" ? s.metadata.nimNestedIn : "";
1673
+ if (!parent) continue;
1674
+ const key = `${parent} ${s.name}`;
1675
+ if (seen.has(key)) continue;
1676
+ seen.add(key);
1677
+ relationships.push({
1678
+ source: parent,
1679
+ target: s.name,
1680
+ type: ASTEngine.RelationshipType.Contains,
1681
+ confidence: 1,
1682
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
1683
+ metadata: { relationshipType: "nim_contains", nimNestedRoutine: true }
1684
+ });
1685
+ }
1686
+ }
1687
+ /**
1688
+ * `decorates` — a declaration carrying a pragma that names a FILE-LOCAL pragma TEMPLATE.
1689
+ *
1690
+ * ## WHY THIS EXISTS, AND WHY IT IS NARROW ON PURPOSE (2026-10-23, rung R3)
1691
+ *
1692
+ * The lane's first plan was to RULE `decorates` ontological, on the csharp/fsharp precedent
1693
+ * ("an annotation is passive metadata; a decorator is an applied callable"). The census
1694
+ * (`script/m14/nim-extended-census.ts`) MEASURED THE FALSIFIER AND IT FIRED: over the whole
1695
+ * 6,063-file admitted population, 294 pragma-use sites name a DECLARED pragma template — 48
1696
+ * distinct `template X() {.pragma.}` / `macro X(...) {.pragma.}` declarations in 20 files — and
1697
+ * a pragma template is INVOKED BY THE COMPILER at the site that names it. Those sites are the
1698
+ * dictionary's decorator (`src/ast/types.ts:131-144`: decorated declaration → applied callable),
1699
+ * and the ruling was WITHDRAWN BY ITS OWN FALSIFIER rather than published. The other 99.1 % of
1700
+ * pragma name sites (`{.inline.}`, `{.cdecl.}`, `{.raises.}`, …) are COMPILER DIRECTIVES and are
1701
+ * deliberately NOT emitted: sweeping them in would turn a 0.9 % construct into a 33,266-row lie.
1702
+ *
1703
+ * ## THE DISCRIMINATOR, AND THE PER-FILE CAVEAT
1704
+ *
1705
+ * A pragma name emits `decorates` ONLY when it resolves to a pragma template DECLARED IN THIS
1706
+ * FILE. Same-file uses resolve (that is the construct a per-file analyzer can prove). A template
1707
+ * imported from ANOTHER file is out of a per-file analyzer's reach, so its `decorates` row is not
1708
+ * invented — this is the lane's standard per-file limitation (NIMD2's overrides scope, and the
1709
+ * same caveat ruby and csharp carry), stated on the page rather than hidden here.
1710
+ *
1711
+ * ## THE CONTRACT DETAILS
1712
+ *
1713
+ * · SOURCE = the decorated declaration's PUBLISHED symbol spelling (`qualifiedName` where the
1714
+ * symbol has one — a method is `Receiver.method`, exactly as the `overrides` endpoints are
1715
+ * spelled, because a bare method name cannot address either end), TARGET = the pragma
1716
+ * template name. Direction is the contract's (`types.ts:131-144`).
1717
+ * · NO ROW when the decorated declaration is not a published symbol: the vocabulary proposal's
1718
+ * decorates MUST-NOT ("no row when no published symbol is decorated") is implemented by the
1719
+ * symbol lookup below, not by a claim.
1720
+ * · `metadata.decoratorConstruct: "pragma_template"` is what tells a consumer this row is the
1721
+ * applied-callable class rather than a hypothetical annotation sweep.
1722
+ * · The pragma NAME stays on the symbol's `decorators` payload as well: it is the evidence, and
1723
+ * the java/dart lanes' rule (annotation mentions live in metadata) is not contradicted — this
1724
+ * row says the pragma is an applied callable, which those lanes' annotations are not.
1725
+ */
1726
+ collectPragmaDecorates(rootNode, symbols, relationships) {
1727
+ const pragmaTemplates = /* @__PURE__ */ new Set();
1728
+ const templateStack = [rootNode];
1729
+ while (templateStack.length > 0) {
1730
+ const n = templateStack.pop();
1731
+ if (!n) continue;
1732
+ if (n.type === "template_declaration" || n.type === "macro_declaration") {
1733
+ const list = n.childForFieldName("pragmas") ?? this.firstChildOfType(n, "pragma_list");
1734
+ if (list && this.pragmaNameNodes(list).some((p) => p.name === "pragma")) {
1735
+ const nameNode = n.childForFieldName("name");
1736
+ if (nameNode) {
1737
+ const info = this.readNameNode(nameNode);
1738
+ if (info.name) pragmaTemplates.add(info.name);
1739
+ }
1740
+ }
1741
+ }
1742
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1743
+ const c = n.namedChild(i);
1744
+ if (c) templateStack.push(c);
1745
+ }
1746
+ }
1747
+ if (pragmaTemplates.size === 0) return;
1748
+ const symbolsByStart = /* @__PURE__ */ new Map();
1749
+ for (const s of symbols) {
1750
+ const line = s.range?.startLine;
1751
+ if (typeof line === "number" && s.name) {
1752
+ let a = symbolsByStart.get(line);
1753
+ if (!a) {
1754
+ a = [];
1755
+ symbolsByStart.set(line, a);
1756
+ }
1757
+ a.push(s);
1758
+ }
1759
+ }
1760
+ const DECORATABLE = /* @__PURE__ */ new Set([
1761
+ "proc_declaration",
1762
+ "func_declaration",
1763
+ "method_declaration",
1764
+ "iterator_declaration",
1765
+ "template_declaration",
1766
+ "macro_declaration",
1767
+ "converter_declaration",
1768
+ "type_symbol_declaration",
1769
+ "type_declaration",
1770
+ "enum_declaration",
1771
+ "object_declaration",
1772
+ "variable_declaration",
1773
+ "field_declaration"
1774
+ ]);
1775
+ const stack = [rootNode];
1776
+ while (stack.length > 0) {
1777
+ const n = stack.pop();
1778
+ if (!n) continue;
1779
+ if (DECORATABLE.has(n.type)) {
1780
+ const list = n.childForFieldName("pragmas") ?? n.childForFieldName("pragma") ?? this.firstChildOfType(n, "pragma_list");
1781
+ if (list) {
1782
+ const nameNode = n.childForFieldName("name");
1783
+ const own = nameNode ? this.readNameNode(nameNode).name : "";
1784
+ const candidates = symbolsByStart.get(n.startPosition.row + 1) ?? [];
1785
+ const sym = own ? candidates.find((c) => c.name === own) : candidates[0];
1786
+ if (sym) {
1787
+ for (const p of this.pragmaNameNodes(list)) {
1788
+ if (!pragmaTemplates.has(p.name)) continue;
1789
+ relationships.push({
1790
+ source: sym.qualifiedName ?? sym.name,
1791
+ target: p.name,
1792
+ type: ASTEngine.RelationshipType.Decorates,
1793
+ confidence: 1,
1794
+ line_number: p.line,
1795
+ metadata: {
1796
+ relationshipType: "nim_pragma_template",
1797
+ decoratorConstruct: "pragma_template",
1798
+ isDecorator: true,
1799
+ // Read straight off the declaration's `pragmas` field — a structural declaration,
1800
+ // the tier `extends` and `implements` carry for the same reason.
1801
+ evidenceClass: "structural"
1802
+ }
1803
+ });
1804
+ }
1805
+ }
1806
+ }
1807
+ }
1808
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1809
+ const c = n.namedChild(i);
1810
+ if (c) stack.push(c);
1811
+ }
1812
+ }
1813
+ }
1814
+ /**
1815
+ * `extends` — `type D = ref object of B`. The `inherits` field is a
1816
+ * `type_expression`; its leaf is the base type.
1817
+ *
1818
+ * The base is NOT required to be declared in this file. In a real Nim module
1819
+ * the base almost never is: `Named = ref object of Point` with `Point` in a
1820
+ * sibling file is the normal shape, and requiring a same-file declaration
1821
+ * dropped the whole family at the seam — measured: a three-file module whose
1822
+ * subclass lived in a different file from its base persisted ZERO `extends`
1823
+ * rows. The edge is emitted whenever the base is either declared in this file
1824
+ * OR reachable through an import, and the evidence tier says which: a
1825
+ * same-file base is `structural`, an imported one is `import_based` so the
1826
+ * resolver never mistakes a cross-module base for a local one.
1827
+ *
1828
+ * `ref object of RootObj` names the compiler's own `RootObj`, which is
1829
+ * neither declared nor imported — a name with no referent — so it is
1830
+ * deliberately NOT emitted: an `extends` to a non-existent symbol is the
1831
+ * phantom this guard exists to prevent.
1832
+ */
1833
+ collectInheritance(symbols, relationships, fileTypes) {
1834
+ for (const s of symbols) {
1835
+ const base = s.metadata?.nimInherits;
1836
+ if (typeof base !== "string" || !base) continue;
1837
+ if (base === s.name) continue;
1838
+ if (NIM_COMPILER_ROOTS.has(base)) continue;
1839
+ const sameFile = fileTypes.has(base);
1840
+ relationships.push({
1841
+ source: s.name,
1842
+ target: base,
1843
+ type: ASTEngine.RelationshipType.Extends,
1844
+ confidence: 1,
1845
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
1846
+ metadata: { relationshipType: "nim_object_inheritance", ...sameFile ? {} : { nimCrossModuleBase: true } }
1847
+ });
1848
+ }
1849
+ }
1850
+ /**
1851
+ * `implements` — `type A = concept x of B`.
1852
+ *
1853
+ * ## Why this is `implements` and not `extends`
1854
+ *
1855
+ * A Nim `concept` is the language's only interface-like construct, it is
1856
+ * satisfied STRUCTURALLY and implicitly (no type is ever annotated as
1857
+ * implementing it), and the `of` clause is a declaration that A's contract
1858
+ * includes B's. That is the same fact the elixir lane publishes as
1859
+ * `implements` for a `@impl Protocol` — a duck-typed contract, satisfied
1860
+ * without naming — and the same fact graphql publishes for `type X implements
1861
+ * Y`. `extends` in this analyzer is already taken by the nominal class
1862
+ * hierarchy (`object_declaration.inherits`), and this edge is emphatically not
1863
+ * that: there is no vtable, no base type and no nominal subtype relation
1864
+ * anywhere in a concept refinement.
1865
+ *
1866
+ * The two labels are not interchangeable here, and conflating them is exactly
1867
+ * the confusion question 4 asks about: `A extends B` says A's storage contains
1868
+ * B's; `A implements B` says anything satisfying A satisfies B.
1869
+ *
1870
+ * ## Why the base need not be declared in this file
1871
+ *
1872
+ * Same decision as `collectInheritance`, for the same reason: a concept
1873
+ * refining a contract from a sibling module is ordinary Nim, and requiring a
1874
+ * same-file declaration would drop the family the way it dropped cross-file
1875
+ * `extends` at the seam (measured there: a three-file module persisted ZERO
1876
+ * `extends` rows). The edge carries `nimCrossModuleContract` so a reader can
1877
+ * tell a locally-declared contract from an imported one; the store resolves
1878
+ * the far end.
1879
+ *
1880
+ * ## Confidence
1881
+ *
1882
+ * 1.0, not a guess. The `refines` field names the contract outright, in one
1883
+ * spelling, with no name matching and no inference — the same grounds on which
1884
+ * `extends` is 1.0 and the f#/graphql interface edges are 1.0. The
1885
+ * `overrides` companion edge is 0.9 for a different reason: that one walks a
1886
+ * PARTLY VISIBLE hierarchy, and this one does not walk anything.
1887
+ *
1888
+ * ## What it deliberately does NOT emit
1889
+ *
1890
+ * Concept SATISFACTION by an ordinary type (`type T = object` that happens to
1891
+ * have `+` and `in`) is not emitted, and cannot be: it is decided by the
1892
+ * compiler's structural matching at instantiation time, with no syntax naming
1893
+ * the relationship. Inventing those edges is precisely the fabrication the
1894
+ * workstream forbids — they would be unresolvable at both ends and would poison
1895
+ * `bb_impact`. The `references` edge for the same `of` clause is deliberately
1896
+ * KEPT, mirroring what this analyzer already does for `inherits` (which yields
1897
+ * both `extends` and `references`): the ontology edge answers "which contract
1898
+ * does this claim" and the type-graph edge answers "which types does this
1899
+ * depend on", and those are two questions.
1900
+ */
1901
+ collectContractRefinement(rootNode, symbols, relationships) {
1902
+ const localTypes = /* @__PURE__ */ new Set();
1903
+ for (const s of symbols) if (s.kind === ASTEngine.SymbolKind.Interface) localTypes.add(s.name);
1904
+ if (localTypes.size === 0) return;
1905
+ for (const s of symbols) {
1906
+ const refines = s.metadata?.nimRefines;
1907
+ if (!Array.isArray(refines) || refines.length === 0) continue;
1908
+ for (const raw of refines) {
1909
+ const target = String(raw ?? "");
1910
+ if (!target) continue;
1911
+ if (target === s.name) continue;
1912
+ const sameFile = localTypes.has(target);
1913
+ relationships.push({
1914
+ source: s.name,
1915
+ target,
1916
+ type: ASTEngine.RelationshipType.Implements,
1917
+ confidence: 1,
1918
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
1919
+ metadata: {
1920
+ relationshipType: "nim_concept_refinement",
1921
+ nimContractKind: "concept",
1922
+ ...sameFile ? {} : { nimCrossModuleContract: true }
1923
+ }
1924
+ });
1925
+ }
1926
+ }
1927
+ this.collectIsFormRefinement(rootNode, symbols, relationships, localTypes);
1928
+ }
1929
+ /**
1930
+ * `implements`, second spelling — the `is` form of concept refinement.
1931
+ *
1932
+ * ## The two spellings, and which one real code uses
1933
+ *
1934
+ * `nim/doc/manual_experimental.md:1205-1213` names them as alternatives for
1935
+ * one fact: "When the matched type within a concept is directly tested against
1936
+ * a different concept, we say that the outer concept is a refinement of the
1937
+ * inner concept … As an alternative way of defining concept refinements, you
1938
+ * can use the object inheritance syntax involving the `of` keyword". So
1939
+ *
1940
+ * type B = concept x
1941
+ * …
1942
+ * type A = concept x of B # the `of` form (collectContractRefinement)
1943
+ * type A = concept x
1944
+ * x is B # the `is` form (here)
1945
+ *
1946
+ * state the same relationship. The `of` clause is the rarer of the two: over
1947
+ * 4,101 corpus files it occurs ZERO times while the corpus holds 216 concept
1948
+ * declarations, so an analyzer that read only the clause would report the type
1949
+ * as absent from a corpus that is full of concepts.
1950
+ *
1951
+ * ## The three guards, and why each is load-bearing
1952
+ *
1953
+ * The `is` form is a much wider syntax than the `of` clause, so each guard
1954
+ * removes a class of statement that is NOT a refinement:
1955
+ *
1956
+ * 1. The LEFT side must be one of the concept's own formal parameters. In a
1957
+ * concept body `o.foo is B` tests the type of a FIELD named `foo`, which
1958
+ * constrains that field and refines nothing — the corpus has exactly this
1959
+ * case (`nim/tests/concepts/texplain.nim:136`, `o.foo is RegularConcept`),
1960
+ * and reading it as a refinement would be a fabricated edge.
1961
+ * 2. The RIGHT side must be a concept THIS FILE declares. `x is int` is a
1962
+ * type constraint on the matched parameter, and `int` implements nothing.
1963
+ * Measured on the corpus: of 125 `is X` expressions in concept bodies, the
1964
+ * most frequent right-hand sides are `T` (31), `int` (23), `bool` (15),
1965
+ * `string` (10), `V` (5), `float` (5) — a generic parameter or a concrete
1966
+ * type in every one of those, and emitting `implements -> int` for them is
1967
+ * the fabrication the workstream forbids. Only 5 name a same-file concept,
1968
+ * so the guard drops 120 of 125 rather than 0.
1969
+ * 3. A concept naming ITSELF is skipped, as on the `of` path. All 5 corpus
1970
+ * cases are this shape (`nim/tests/concepts/trecursive_concepts.nim:26`,
1971
+ * `distinctBase(x) is Primitive`; `tconcepts_issues.nim:421`,
1972
+ * `bend(x is Bendable)`), which is why this family's corpus count is 0
1973
+ * edges rather than 5.
1974
+ *
1975
+ * An imported contract is NOT guessed at, even though `extends` does allow a
1976
+ * cross-module base here: the same spelling names a concept or a concrete type
1977
+ * depending on a scope this pass cannot see, so the guard is not relaxed for a
1978
+ * confidence discount. That is the declared limit, with its measurement.
1979
+ *
1980
+ * Confidence 1.0 on the same grounds as the `of` form: the declaration names
1981
+ * the contract outright, and guards 1-3 have already excluded every reading in
1982
+ * which that name could be something else.
1983
+ */
1984
+ collectIsFormRefinement(rootNode, symbols, relationships, localConcepts) {
1985
+ if (localConcepts.size === 0) return;
1986
+ const sites = [];
1987
+ const walk = (node) => {
1988
+ if (node.type === "type_declaration") {
1989
+ const decl = node.namedChildCount > 0 ? node.namedChild(0) : null;
1990
+ if (decl && decl.type === "type_symbol_declaration") {
1991
+ const nameNode = decl.childForFieldName("name");
1992
+ const concept = this.conceptDeclarationIn(node);
1993
+ if (nameNode && concept) {
1994
+ const params = /* @__PURE__ */ new Set();
1995
+ for (const p of this.readConceptParameters(concept)) if (p.name) params.add(p.name);
1996
+ sites.push({
1997
+ name: this.readNameNode(nameNode).name,
1998
+ params,
1999
+ body: this.conceptBody(concept)
2000
+ });
2001
+ }
2002
+ }
2003
+ }
2004
+ for (let i = node.childCount - 1; i >= 0; i--) {
2005
+ const c = node.child(i);
2006
+ if (c) walk(c);
2007
+ }
2008
+ };
2009
+ walk(rootNode);
2010
+ if (sites.length === 0) return;
2011
+ for (const site of sites) {
2012
+ if (!site.name || !site.body) continue;
2013
+ const contracts = this.isFormContracts(site.body, site.params, localConcepts);
2014
+ for (const contract of contracts) {
2015
+ if (contract === site.name) continue;
2016
+ relationships.push({
2017
+ source: site.name,
2018
+ target: contract,
2019
+ type: ASTEngine.RelationshipType.Implements,
2020
+ confidence: 1,
2021
+ line_number: site.body.startPosition.row + 1,
2022
+ metadata: {
2023
+ relationshipType: "nim_concept_refinement",
2024
+ nimContractKind: "concept",
2025
+ nimRefinementSpelling: "is"
2026
+ }
2027
+ });
2028
+ }
2029
+ }
2030
+ void symbols;
2031
+ }
2032
+ /** The `concept_declaration` inside a `type_declaration`'s right-hand side, if
2033
+ * the declaration is a concept at all. */
2034
+ conceptDeclarationIn(typeDecl) {
2035
+ const find = (n) => {
2036
+ if (n.type === "concept_declaration") return n;
2037
+ for (let i = 0; i < n.childCount; i++) {
2038
+ const c = n.child(i);
2039
+ if (!c) continue;
2040
+ const hit = find(c);
2041
+ if (hit) return hit;
2042
+ }
2043
+ return null;
2044
+ };
2045
+ return find(typeDecl);
2046
+ }
2047
+ /** A concept's body — the LAST named child, which the grammar puts after the
2048
+ * optional parameter list and the optional `of` clause. */
2049
+ conceptBody(concept) {
2050
+ for (let i = concept.namedChildCount - 1; i >= 0; i--) {
2051
+ const c = concept.namedChild(i);
2052
+ if (c) return c;
2053
+ }
2054
+ return null;
2055
+ }
2056
+ /** The contracts a concept body tests its matched parameter against with `is`,
2057
+ * keeping only names that are a concept of this file.
2058
+ *
2059
+ * Two structural guards, and both are needed because the `is` form is a much
2060
+ * wider syntax than the `of` clause:
2061
+ *
2062
+ * - the LEFT side must be a BARE formal parameter. `o.length is Sized` tests a
2063
+ * field's type (`nim/tests/concepts/texplain.nim:136`), and
2064
+ * `distinctBase(x) is Primitive` tests the result of a call
2065
+ * (`trecursive_concepts.nim:26`); neither refines a contract, and both are
2066
+ * rejected because their left side is not the parameter itself. This one
2067
+ * guard covers a chain (`x is Shape or name(x) is Sized`) without needing to
2068
+ * understand `or`/`and`: the second operand's left side is a call, so it
2069
+ * fails here.
2070
+ * - an `argument_list` is not descended into, because `bend(x is Bendable)`
2071
+ * states a constraint passed to a call rather than a refinement. The corpus
2072
+ * has that shape too (`tconcepts_issues.nim:421`).
2073
+ */
2074
+ isFormContracts(body, params, localConcepts) {
2075
+ const out = [];
2076
+ const seen = /* @__PURE__ */ new Set();
2077
+ const walk = (node) => {
2078
+ if (node.type === "argument_list") return;
2079
+ if (node.type === "infix_expression") {
2080
+ const op = node.childForFieldName("operator");
2081
+ const left = node.childForFieldName("left");
2082
+ const right = node.childForFieldName("right");
2083
+ if (op && op.text === "is" && left && left.type === "identifier" && params.has(left.text) && right && right.type === "identifier") {
2084
+ const name = right.text;
2085
+ if (localConcepts.has(name) && !seen.has(name)) {
2086
+ seen.add(name);
2087
+ out.push(name);
2088
+ }
2089
+ }
2090
+ }
2091
+ for (let i = 0; i < node.childCount; i++) {
2092
+ const c = node.child(i);
2093
+ if (c) walk(c);
2094
+ }
2095
+ };
2096
+ walk(body);
2097
+ return out;
2098
+ }
2099
+ /**
2100
+ * `overrides` — Nim method dispatch is dynamic, so overriding is real. The
2101
+ * derivation is AST-only and file-local: `declaredSubtypes` comes from
2102
+ * `object_declaration.inherits`, so a `method m(d: Dog)` overrides a
2103
+ * `method m(a: Animal)` exactly when this file declares `Dog` as a subtype of
2104
+ * `Animal`. An overload SET (`method m(d: Dog)` and `method m(d: Cat)`) emits
2105
+ * one edge per candidate and marks the edge ambiguous rather than guessing.
2106
+ *
2107
+ * Cross-file and system-library bases are NOT inferable without a semantic
2108
+ * index, and are declared as an explicit limit rather than guessed from name
2109
+ * shape.
2110
+ */
2111
+ collectOverrides(symbols, relationships, declaredSubtypes, companion) {
2112
+ if (declaredSubtypes.size === 0 && !companion) return;
2113
+ const byKey = /* @__PURE__ */ new Map();
2114
+ for (const s of symbols) {
2115
+ if (s.kind !== ASTEngine.SymbolKind.Method) continue;
2116
+ const receiver = String(s.metadata?.nimReceiverType ?? "");
2117
+ if (!receiver) continue;
2118
+ const key = `${receiver} ${s.name}`;
2119
+ const arr = byKey.get(key) ?? [];
2120
+ arr.push(s);
2121
+ byKey.set(key, arr);
2122
+ }
2123
+ if (byKey.size === 0) return;
2124
+ const emitted = /* @__PURE__ */ new Set();
2125
+ for (const [key, group] of byKey) {
2126
+ const sep = key.indexOf(" ");
2127
+ const receiver = key.slice(0, sep);
2128
+ const name = key.slice(sep + 1);
2129
+ for (const superType of this.transitiveSupertypes(receiver, declaredSubtypes)) {
2130
+ const bases = byKey.get(`${superType} ${name}`);
2131
+ if (!bases) continue;
2132
+ for (const b of bases) {
2133
+ if (superType === receiver) continue;
2134
+ const sourceLabel = `${receiver}.${name}`;
2135
+ const targetLabel = `${superType}.${name}`;
2136
+ const k = `${sourceLabel} ${targetLabel}`;
2137
+ if (emitted.has(k)) continue;
2138
+ emitted.add(k);
2139
+ relationships.push({
2140
+ source: sourceLabel,
2141
+ target: targetLabel,
2142
+ type: ASTEngine.RelationshipType.Overrides,
2143
+ confidence: 1,
2144
+ ...b.range?.startLine ? { line_number: b.range.startLine } : {},
2145
+ metadata: { relationshipType: "nim_method_override", receiverType: receiver, baseReceiverType: superType }
2146
+ });
2147
+ }
2148
+ }
2149
+ }
2150
+ if (!companion || byKey.size === 0) return;
2151
+ const parentsOf = /* @__PURE__ */ new Map();
2152
+ for (const [base, subs] of declaredSubtypes) {
2153
+ for (const sub of subs) {
2154
+ const arr = parentsOf.get(sub) ?? [];
2155
+ if (arr.indexOf(base) === -1) arr.push(base);
2156
+ parentsOf.set(sub, arr);
2157
+ }
2158
+ }
2159
+ const getParents = (typeName) => parentsOf.get(typeName) ?? [];
2160
+ for (const [key, group] of byKey) {
2161
+ const sep = key.indexOf(" ");
2162
+ const receiver = key.slice(0, sep);
2163
+ const name = key.slice(sep + 1);
2164
+ const startParents = parentsOf.get(receiver) ?? [];
2165
+ if (startParents.length === 0) continue;
2166
+ const declaring = findDeclaringParent(name, startParents, companion, getParents);
2167
+ if (!declaring || declaring === receiver) continue;
2168
+ const sourceLabel = `${receiver}.${name}`;
2169
+ const targetLabel = `${declaring}.${name}`;
2170
+ const k = `${sourceLabel} ${targetLabel}`;
2171
+ if (emitted.has(k)) continue;
2172
+ emitted.add(k);
2173
+ relationships.push({
2174
+ source: sourceLabel,
2175
+ target: targetLabel,
2176
+ type: ASTEngine.RelationshipType.Overrides,
2177
+ // 0.9, not 1: the companion asserts that the other module DECLARES the
2178
+ // method, which is the substance of the edge, but the walk could not
2179
+ // descend past a base that is itself in a third module. The
2180
+ // differential's 0.7 claim threshold keeps this a claim rather than an
2181
+ // assertion, which is the honest reading of a partly-visible hierarchy.
2182
+ confidence: 0.9,
2183
+ ...group[0]?.range?.startLine ? { line_number: group[0].range.startLine } : {},
2184
+ metadata: {
2185
+ relationshipType: "nim_method_override",
2186
+ receiverType: receiver,
2187
+ baseReceiverType: declaring,
2188
+ nimCrossFileBase: true,
2189
+ nimResolution: "companion-index"
2190
+ }
2191
+ });
2192
+ }
2193
+ }
2194
+ /** Every strict supertype of `leaf` reachable through the declared-subtype map. */
2195
+ transitiveSupertypes(leaf, declaredSubtypes) {
2196
+ const supers = /* @__PURE__ */ new Set();
2197
+ const seen = /* @__PURE__ */ new Set([leaf]);
2198
+ const queue = [leaf];
2199
+ while (queue.length > 0) {
2200
+ const cur = queue.pop();
2201
+ for (const [base, subs] of declaredSubtypes) {
2202
+ if (!subs.has(cur)) continue;
2203
+ if (seen.has(base)) continue;
2204
+ seen.add(base);
2205
+ supers.add(base);
2206
+ queue.push(base);
2207
+ }
2208
+ }
2209
+ return supers;
2210
+ }
2211
+ /**
2212
+ * `calls` / `uses` — the two value-flow families.
2213
+ *
2214
+ * `calls` — a `call` / `dot_generic_call` node: the target is the callee LEAF
2215
+ * (shared-callee family rule — `a.b.c()` targets `c`), read from
2216
+ * the grammar's `function` field. Command syntax (`echo "x"`) is
2217
+ * the same node. A `bracket_expression` callee (`Foo[int](x)`,
2218
+ * `parseJson[int](s)`) is a generic INSTANTIATION and its target is
2219
+ * the `left` leaf. Compiler intrinsics that Nim does not allow a
2220
+ * user to overload are filtered (see NIM_BUILTINS).
2221
+ *
2222
+ * `uses` — a READ of a name this file declares, from two positions:
2223
+ * (a) a bare `identifier` in an expression position, and
2224
+ * (b) the member leaf of a `dot_expression` that is not a call
2225
+ * callee (Nim's UFCS makes `x.foo` a member read). Resolution
2226
+ * requires the name to be declared in THIS file, because Nim's
2227
+ * declare-before-use rule makes anything else either an import or
2228
+ * an error. A lexical scope stack (parameters, generic
2229
+ * parameters, block/branch/loop bindings) suppresses shadowed
2230
+ * names so a local never swallows a file-level value.
2231
+ */
2232
+ collectCallsAndUses(rootNode, relationships, ctx) {
2233
+ const emitted = /* @__PURE__ */ new Set();
2234
+ const instantiated = /* @__PURE__ */ new Set();
2235
+ const typeRefEmitted = /* @__PURE__ */ new Set();
2236
+ const typeRefHelpers = this.makeDeclaredTypeRefHelpers(
2237
+ relationships,
2238
+ ctx.fileTypes,
2239
+ ctx.genericParamNames,
2240
+ ctx.fileValues,
2241
+ ctx.fileCallables,
2242
+ typeRefEmitted
2243
+ );
2244
+ const push = (source, target, type, confidence, line, extra) => {
2245
+ const key = `${source} ${type} ${target}`;
2246
+ if (emitted.has(key)) return;
2247
+ emitted.add(key);
2248
+ relationships.push({
2249
+ source,
2250
+ target,
2251
+ type,
2252
+ confidence,
2253
+ line_number: line,
2254
+ metadata: extra
2255
+ });
2256
+ };
2257
+ const scopeStack = [];
2258
+ const top = () => scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
2259
+ const isShadowed = (name) => {
2260
+ for (let i = scopeStack.length - 1; i >= 0; i--) if (scopeStack[i].names.has(name)) return true;
2261
+ return false;
2262
+ };
2263
+ const currentFn = () => {
2264
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
2265
+ const name = scopeStack[i].fnName;
2266
+ if (name) return name;
2267
+ }
2268
+ return "(global)";
2269
+ };
2270
+ const resolvable = (name) => ctx.fileValues.has(name) || ctx.fileEnumMembers.has(name) || ctx.fileFieldNames.has(name);
2271
+ const stack = [{ node: rootNode, exit: false, pushed: false }];
2272
+ while (stack.length > 0) {
2273
+ const { node, exit, pushed } = stack.pop();
2274
+ if (exit) {
2275
+ if (pushed) scopeStack.pop();
2276
+ continue;
2277
+ }
2278
+ const type = node.type;
2279
+ let pushedScope = false;
2280
+ this.collectInstantiationAt(node, relationships, ctx.fileTypes, ctx.fileCallables, instantiated, currentFn());
2281
+ this.collectDeclaredTypeRefAt(node, typeRefHelpers);
2282
+ const callable = this.callableNodes[type];
2283
+ if (callable) {
2284
+ const frame = { names: /* @__PURE__ */ new Set(), fnName: null };
2285
+ const nameNode = node.childForFieldName("name");
2286
+ const info = nameNode ? this.readNameNode(nameNode) : null;
2287
+ frame.fnName = info?.name ?? null;
2288
+ for (const p of this.readParameters(node)) if (p.name && p.name !== "_") frame.names.add(p.name);
2289
+ for (const g of this.readGenericParameterNames(node)) frame.names.add(g);
2290
+ scopeStack.push(frame);
2291
+ pushedScope = true;
2292
+ } else if (type === "concept_declaration") {
2293
+ const frame = { names: /* @__PURE__ */ new Set(), fnName: null };
2294
+ const ownerDecl = node.parent && node.parent.type === "type_declaration" ? node.parent.namedChild(0) : null;
2295
+ if (ownerDecl) {
2296
+ const n = ownerDecl.childForFieldName("name");
2297
+ const info = n ? this.readNameNode(n) : null;
2298
+ frame.fnName = info?.name ?? null;
2299
+ }
2300
+ const params = this.firstChildOfType(node, "parameter_list");
2301
+ if (params) {
2302
+ for (let i = 0; i < params.namedChildCount; i++) {
2303
+ const c = params.namedChild(i);
2304
+ if (c && (c.type === "identifier" || c.type === "accent_quoted") && c.text !== "_") frame.names.add(c.text);
2305
+ }
2306
+ }
2307
+ scopeStack.push(frame);
2308
+ pushedScope = true;
2309
+ } else if (type === "statement_list" || type === "block" || type === "do_block" || type === "term_rewriting_pattern") {
2310
+ scopeStack.push({ names: /* @__PURE__ */ new Set(), fnName: null });
2311
+ pushedScope = true;
2312
+ } else if (type === "of_branch" || type === "except_branch" || type === "elif_branch" || type === "else_branch" || type === "finally_branch") {
2313
+ scopeStack.push({ names: /* @__PURE__ */ new Set(), fnName: null });
2314
+ pushedScope = true;
2315
+ } else if (type === "variable_declaration") {
2316
+ const frame = top();
2317
+ if (frame) {
2318
+ const list = node.namedChildCount > 0 ? node.namedChild(0) : null;
2319
+ if (list && list.type === "symbol_declaration_list") {
2320
+ for (const bound of this.readBoundDeclarationNames(list)) frame.names.add(bound);
2321
+ }
2322
+ }
2323
+ } else if (type === "for") {
2324
+ scopeStack.push({ names: /* @__PURE__ */ new Set(), fnName: null });
2325
+ pushedScope = true;
2326
+ const left = node.childForFieldName("left");
2327
+ if (left) {
2328
+ const frame = top();
2329
+ for (const d of this.symbolNamesIn(left)) if (d && frame) frame.names.add(d);
2330
+ }
2331
+ }
2332
+ if (type === "except_branch") {
2333
+ const values = node.childForFieldName("values");
2334
+ if (values) {
2335
+ const frame = top();
2336
+ for (let i = 0; i < values.namedChildCount; i++) {
2337
+ const v = values.namedChild(i);
2338
+ if (v && v.type === "infix_expression") {
2339
+ const op = v.childForFieldName("operator");
2340
+ if (op && op.type === "as") {
2341
+ const r = v.childForFieldName("right");
2342
+ if (r && r.type === "identifier" && frame) frame.names.add(r.text);
2343
+ }
2344
+ }
2345
+ }
2346
+ }
2347
+ }
2348
+ if (type === "call") {
2349
+ const fn = node.childForFieldName("function");
2350
+ const leafForGate = fn ? this.calleeLeafNode(fn) : null;
2351
+ const isTypeConstruction = !!leafForGate && ctx.fileTypes.has(leafForGate.text) && !ctx.fileCallables.has(leafForGate.text);
2352
+ if (fn && this.isParameterModifierCall(node) && this.isInTypePosition(node)) {
2353
+ } else if (isTypeConstruction) {
2354
+ } else {
2355
+ const leaf = fn ? this.calleeLeafNode(fn) : void 0;
2356
+ const leafText = leaf?.text;
2357
+ const isDocPragma = leafText === "runnableExamples" && this.runnableExamplesArgument(node) !== null;
2358
+ if (leafText && leafText !== "_" && !isDocPragma && REAL_NIM_BUILTINS.indexOf(leafText) === -1 && !isKeywordLike(leafText)) {
2359
+ const source = currentFn();
2360
+ const importBased = fn ? this.isImportQualified(fn, ctx.importQualifiers) : false;
2361
+ push(source, leafText, ASTEngine.RelationshipType.Calls, importBased ? 0.75 : 0.85, leaf ? leaf.startPosition.row + 1 : node.startPosition.row + 1, {
2362
+ relationshipType: "nim_call",
2363
+ callExpression: this.nodeText(node).slice(0, 60),
2364
+ ...importBased ? { nimImportBased: true } : {}
2365
+ });
2366
+ }
2367
+ }
2368
+ } else if (type === "dot_generic_call") {
2369
+ const fn = node.childForFieldName("function");
2370
+ const leafText = fn?.text;
2371
+ if (leafText && leafText !== "_" && REAL_NIM_BUILTINS.indexOf(leafText) === -1) {
2372
+ push(currentFn(), leafText, ASTEngine.RelationshipType.Calls, 0.85, fn ? fn.startPosition.row + 1 : node.startPosition.row + 1, {
2373
+ relationshipType: "nim_dot_generic_call",
2374
+ callExpression: this.nodeText(node).slice(0, 60)
2375
+ });
2376
+ }
2377
+ }
2378
+ if (type === "dot_expression") {
2379
+ const leaf = this.dotLeafNode(node);
2380
+ const leafText = leaf?.text;
2381
+ const left = node.childForFieldName("left");
2382
+ const inCallCallee = this.isCallCallee(node);
2383
+ const callableName = leafText && (ctx.fileCallables.has(leafText) || ctx.nestedCallables.has(leafText)) ? leafText : null;
2384
+ if (callableName && !inCallCallee) {
2385
+ push(currentFn(), callableName, ASTEngine.RelationshipType.Calls, 0.9, leaf ? leaf.startPosition.row + 1 : node.startPosition.row + 1, {
2386
+ relationshipType: "nim_ufcs_member_call",
2387
+ callExpression: this.nodeText(node).slice(0, 60)
2388
+ });
2389
+ } else if (leafText && leafText !== "_" && !inCallCallee && resolvable(leafText) && !isKeywordLike(leafText)) {
2390
+ const importBased = left ? this.isImportQualified(left, ctx.importQualifiers) : false;
2391
+ push(currentFn(), leafText, ASTEngine.RelationshipType.Uses, 0.75, leaf ? leaf.startPosition.row + 1 : node.startPosition.row + 1, {
2392
+ relationshipType: "nim_member_access",
2393
+ fieldAccessExpression: this.nodeText(node).slice(0, 60),
2394
+ ...importBased ? { nimImportBased: true } : {}
2395
+ });
2396
+ }
2397
+ }
2398
+ if (type === "identifier") {
2399
+ const name = node.text;
2400
+ if (name !== "_" && !name.startsWith("gensym") && resolvable(name) && !isKeywordLike(name) && !this.isDeclarationName(node) && !this.isConceptParameterPosition(node) && !this.isCallCalleeSubtree(node) && !this.isDotLeaf(node) && !this.isInTypePosition(node) && !this.isModulePathPosition(node) && !this.isLabelPosition(node) && !this.isColonLabel(node) && !this.isExceptAliasPosition(node) && !this.isAssignmentTarget(node) && !this.isAssignmentTarget(node, true) && !isShadowed(name)) {
2401
+ push(currentFn(), name, ASTEngine.RelationshipType.Uses, 1, node.startPosition.row + 1, {
2402
+ relationshipType: "nim_identifier_usage",
2403
+ nodeType: node.type
2404
+ });
2405
+ }
2406
+ }
2407
+ stack.push({ node, exit: true, pushed: pushedScope });
2408
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
2409
+ const c = node.namedChild(i);
2410
+ if (c) stack.push({ node: c, exit: false, pushed: false });
2411
+ }
2412
+ }
2413
+ }
2414
+ /**
2415
+ * `references` — the declared-type graph, from the positions the grammar
2416
+ * marks as types: the `type` field of a parameter/field/variant
2417
+ * discriminator/value binding, the `return_type` of a routine or do-block, the
2418
+ * `inherits` of an object, the `refines` of a concept, a `cast[T]`, and the
2419
+ * right-hand side of a type alias.
2420
+ *
2421
+ * Two leaves are collected from a type expression, and both matter in Nim:
2422
+ * - the type CONSTRUCTOR (`Vec`, `Table`) — the same as the C/Rust family
2423
+ * rule;
2424
+ * - each type ARGUMENT (`seq[T]`, `Table[K, V]`, `array[3, T]`) — Nim has
2425
+ * no `operator()` call syntax for types, so `seq[MyType]` is the ONLY way
2426
+ * to depend on `MyType` and dropping the argument would lose the
2427
+ * dependency entirely. This is a documented, NIM-specific extension of the
2428
+ * zig "no generic-application refs" precedent.
2429
+ *
2430
+ * Builtin type names, this file's own generic parameters, and the alias's own
2431
+ * name are excluded, so no edge can point at a name that is not a type.
2432
+ */
2433
+ /**
2434
+ * The `instantiates` half of the fused edge walk, as a per-node handler.
2435
+ *
2436
+ * It used to own a full-tree walk with its own `{node, fn}` stack purely to
2437
+ * learn the enclosing callable — which the fused walk already tracks for the
2438
+ * shadow rules, so the second copy of that state was pure cost.
2439
+ *
2440
+ * `enclosing` arrives as the fused walk's `currentFn()`, which returns the
2441
+ * sentinel "(global)" when no callable is in scope. The original produced
2442
+ * `null` there and wrote `nextEnclosing ?? "(file)"`, so the sentinel is
2443
+ * translated back rather than emitted: an instantiation at file scope has no
2444
+ * callable to name, and the store resolves a relationship's source_symbol_id by
2445
+ * matching the source name against a symbol, so "(global)" would resolve to
2446
+ * nothing while "(file)" is the established spelling for this family.
2447
+ */
2448
+ collectInstantiationAt(node, relationships, fileTypes, fileCallables, emitted, enclosing) {
2449
+ if (node.type !== "call") return;
2450
+ const source = enclosing === "(global)" ? "(file)" : enclosing;
2451
+ const fn = node.childForFieldName("function");
2452
+ const leaf = fn ? this.calleeLeafNode(fn) : null;
2453
+ const leafText = leaf?.text;
2454
+ if (leafText && fileTypes.has(leafText) && !fileCallables.has(leafText)) {
2455
+ const key = leafText;
2456
+ if (!emitted.has(key)) {
2457
+ emitted.add(key);
2458
+ relationships.push({
2459
+ source,
2460
+ target: leafText,
2461
+ type: ASTEngine.RelationshipType.Instantiates,
2462
+ confidence: 0.9,
2463
+ line_number: leaf ? leaf.startPosition.row + 1 : node.startPosition.row + 1,
2464
+ metadata: { relationshipType: "nim_object_construction", callExpression: this.nodeText(node).slice(0, 60), isConstructor: true }
2465
+ });
2466
+ }
2467
+ }
2468
+ if (leafText === "new") {
2469
+ const args = this.firstChildOfType(node, "argument_list");
2470
+ if (args) {
2471
+ for (let i = 0; i < args.namedChildCount; i++) {
2472
+ const a = args.namedChild(i);
2473
+ if (!a) continue;
2474
+ const t = a.type === "identifier" ? a.text : null;
2475
+ if (t && fileTypes.has(t) && !fileCallables.has(t) && !emitted.has(`new:${t}`)) {
2476
+ emitted.add(`new:${t}`);
2477
+ relationships.push({
2478
+ source,
2479
+ target: t,
2480
+ type: ASTEngine.RelationshipType.Instantiates,
2481
+ confidence: 0.9,
2482
+ line_number: a.startPosition.row + 1,
2483
+ metadata: { relationshipType: "nim_new_allocation", isConstructor: true }
2484
+ });
2485
+ }
2486
+ }
2487
+ }
2488
+ }
2489
+ }
2490
+ /**
2491
+ * The declared-type half's closures, built ONCE PER FILE.
2492
+ *
2493
+ * Hoisting these is the whole point of the factory. The first attempt at
2494
+ * fusing this family kept the logic inline in the per-node handler, which meant
2495
+ * `ownerOf` and the recursive `collectFromTypeExpression` were REBUILT ON EVERY
2496
+ * NODE VISIT — they had been built once per file before that. The output was
2497
+ * byte-identical and the change measured 5.5% SLOWER, in a walk that visits
2498
+ * thousands of nodes per file.
2499
+ *
2500
+ * Same lesson as the rejected per-node `Map` memoization, arriving from the
2501
+ * other direction: in a hot walk, allocating anything per node costs more than
2502
+ * the traversal it rides on. So the rule for this walk is one stack, one local
2503
+ * counter, and closures built once.
2504
+ */
2505
+ makeDeclaredTypeRefHelpers(relationships, fileTypes, genericParamNames, fileValues, fileCallables, emitted) {
2506
+ const ownerOf = (start) => {
2507
+ let owner = null;
2508
+ let p = start;
2509
+ while (p) {
2510
+ const callable = this.callableNodes[p.type];
2511
+ if (callable) {
2512
+ const n = p.childForFieldName("name");
2513
+ const info = n ? this.readNameNode(n) : null;
2514
+ if (info?.name) return { name: info.name, kind: "callable" };
2515
+ return null;
2516
+ }
2517
+ if (p.type === "type_declaration") {
2518
+ const symDecl = p.namedChildCount > 0 ? p.namedChild(0) : null;
2519
+ if (symDecl && symDecl.type === "type_symbol_declaration") {
2520
+ const n = symDecl.childForFieldName("name");
2521
+ const info = n ? this.readNameNode(n) : null;
2522
+ if (info?.name) return { name: info.name, kind: "type" };
2523
+ }
2524
+ return null;
2525
+ }
2526
+ if (p.type === "field_declaration" || p.type === "variant_discriminator_declaration") {
2527
+ p = p.parent;
2528
+ continue;
2529
+ }
2530
+ if (p.type === "object_declaration") {
2531
+ p = p.parent;
2532
+ continue;
2533
+ }
2534
+ if (p.type === "statement_list" || p.type === "block" || p.type === "do_block") {
2535
+ p = p.parent;
2536
+ continue;
2537
+ }
2538
+ p = p.parent;
2539
+ }
2540
+ return owner;
2541
+ };
2542
+ const isValueNotType = (leaf) => fileValues.has(leaf) || fileCallables.has(leaf) || NIM_LITERAL_KEYWORDS.indexOf(leaf) !== -1;
2543
+ const insideConceptBody = (n) => {
2544
+ let p = n.parent;
2545
+ while (p) {
2546
+ if (p.type === "concept_declaration") return true;
2547
+ p = p.parent;
2548
+ }
2549
+ return false;
2550
+ };
2551
+ const push = (site, relation, owner) => {
2552
+ if (site.type !== "identifier") return;
2553
+ const leaf = site.text;
2554
+ if (!leaf) return;
2555
+ if (NIM_BUILTIN_TYPES.indexOf(leaf) !== -1) return;
2556
+ if (NIM_TYPE_POSITION_KEYWORDS.indexOf(leaf) !== -1) return;
2557
+ if (leaf === NIM_CONCEPT_SELF && !fileTypes.has(leaf) && insideConceptBody(site)) return;
2558
+ if (genericParamNames.has(leaf)) return;
2559
+ if (isValueNotType(leaf)) return;
2560
+ if (!owner) return;
2561
+ const key = `${owner.name} ${leaf}`;
2562
+ if (emitted.has(key)) return;
2563
+ emitted.add(key);
2564
+ relationships.push({
2565
+ source: owner.name,
2566
+ target: leaf,
2567
+ type: ASTEngine.RelationshipType.References,
2568
+ confidence: 0.9,
2569
+ line_number: site.startPosition.row + 1,
2570
+ metadata: { relationshipType: relation, declaredBy: owner.kind }
2571
+ });
2572
+ };
2573
+ const collectFromTypeExpression = (te, relation, owner) => {
2574
+ if (!te) return;
2575
+ const ctor = this.typeConstructorLeaf(te);
2576
+ if (ctor) push(ctor, relation, owner);
2577
+ for (const arg of this.typeArgumentNodes(te)) {
2578
+ if (arg.type === "identifier") {
2579
+ push(arg, "nim_type_argument", owner);
2580
+ } else {
2581
+ collectFromTypeExpression(arg, "nim_type_argument", owner);
2582
+ }
2583
+ }
2584
+ };
2585
+ return { relationships, fileTypes, genericParamNames, emitted, ownerOf, collectFromTypeExpression, push };
2586
+ }
2587
+ /** The per-node arm of the declared-type family, riding the fused edge walk. */
2588
+ collectDeclaredTypeRefAt(n, helpers) {
2589
+ const { fileTypes, genericParamNames, emitted, ownerOf, collectFromTypeExpression, push } = helpers;
2590
+ switch (n.type) {
2591
+ case "parameter_declaration":
2592
+ case "field_declaration":
2593
+ case "variant_discriminator_declaration":
2594
+ case "variable_declaration":
2595
+ case "cast":
2596
+ case "do_block": {
2597
+ const t = n.childForFieldName("type");
2598
+ if (t) collectFromTypeExpression(t, "nim_declared_type", ownerOf(n));
2599
+ break;
2600
+ }
2601
+ case "proc_declaration":
2602
+ case "func_declaration":
2603
+ case "method_declaration":
2604
+ case "iterator_declaration":
2605
+ case "template_declaration":
2606
+ case "macro_declaration":
2607
+ case "converter_declaration": {
2608
+ const ret = n.childForFieldName("return_type");
2609
+ if (ret) collectFromTypeExpression(ret, "nim_return_type", ownerOf(n));
2610
+ break;
2611
+ }
2612
+ case "object_declaration": {
2613
+ const inh = n.childForFieldName("inherits");
2614
+ if (inh) collectFromTypeExpression(inh, "nim_declared_type", ownerOf(n));
2615
+ break;
2616
+ }
2617
+ case "concept_declaration": {
2618
+ const list = this.refinementListOf(n);
2619
+ for (let i = 0; list && i < list.namedChildCount; i++) collectFromTypeExpression(list.namedChild(i), "nim_declared_type", ownerOf(n));
2620
+ break;
2621
+ }
2622
+ case "type_declaration": {
2623
+ const symDecl = n.namedChildCount > 0 ? n.namedChild(0) : null;
2624
+ if (symDecl && symDecl.type === "type_symbol_declaration") {
2625
+ const selfName = symDecl.childForFieldName("name");
2626
+ const selfText = selfName ? this.readNameNode(selfName).name : "";
2627
+ const owner = selfText ? { name: selfText, kind: "type" } : null;
2628
+ const rhs = this.typeDefinitionRhs(n);
2629
+ if (rhs) {
2630
+ const ctor = this.typeConstructorLeaf(rhs);
2631
+ if (ctor && ctor.text !== selfText) push(ctor, "nim_type_alias", owner);
2632
+ for (const arg of this.typeArgumentNodes(rhs)) {
2633
+ if (arg.type === "identifier") {
2634
+ if (arg.text !== selfText) push(arg, "nim_type_argument", owner);
2635
+ } else {
2636
+ collectFromTypeExpression(arg, "nim_type_argument", owner);
2637
+ }
2638
+ }
2639
+ }
2640
+ }
2641
+ break;
2642
+ }
2643
+ default:
2644
+ break;
2645
+ }
2646
+ }
2647
+ /** #419-D evidence-tier classification (family standard: rust/c/cpp/go/zig
2648
+ * all classify; an unclassified edge cannot be judged by the resolver). */
2649
+ classifyEvidenceTier(relationships) {
2650
+ for (const r of relationships) {
2651
+ const md = r.metadata ?? (r.metadata = {});
2652
+ if (md.evidenceClass) continue;
2653
+ const t = String(r.type ?? "");
2654
+ const rt = String(md.relationshipType ?? "");
2655
+ if (t === "contains" || t === "extends" || t === "implements" || t === "overrides") {
2656
+ md.evidenceClass = "structural";
2657
+ continue;
2658
+ }
2659
+ if (t === "references") {
2660
+ md.evidenceClass = rt === "nim_declared_type" || rt === "nim_type_alias" || rt === "nim_type_argument" ? "reference_qualified" : "import_based";
2661
+ continue;
2662
+ }
2663
+ if (t === "instantiates") {
2664
+ md.evidenceClass = "usage_contextual";
2665
+ continue;
2666
+ }
2667
+ if (t === "exports") {
2668
+ md.evidenceClass = "structural";
2669
+ continue;
2670
+ }
2671
+ md.evidenceClass = rt === "nim_import" || rt === "nim_include" || md.nimImportBased ? "import_based" : "usage_contextual";
2672
+ }
2673
+ }
2674
+ // ═══════════════════════════════════════════════════════════════════════
2675
+ // 5. POSITION HELPERS — every one of these is a grammar field or node type
2676
+ // ═══════════════════════════════════════════════════════════════════════
2677
+ /** The leaf NAME node of a `name` field: `_symbol` (identifier /
2678
+ * accent_quoted) or `exported_symbol` (the same, then `*`). */
2679
+ readNameNode(node) {
2680
+ if (node.type === "exported_symbol") {
2681
+ const inner = node.namedChildCount > 0 ? node.namedChild(0) : null;
2682
+ return {
2683
+ name: inner ? inner.text : "",
2684
+ exported: true,
2685
+ range: inner ? this.nodeRange(inner) : this.nodeRange(node)
2686
+ };
2687
+ }
2688
+ if (node.type === "identifier" || node.type === "accent_quoted") {
2689
+ return { name: node.text, exported: false, range: this.nodeRange(node) };
2690
+ }
2691
+ return { name: node.text, exported: false, range: this.nodeRange(node) };
2692
+ }
2693
+ /** The identifier/leaf a call targets. `call.function` is `_basic_expression`
2694
+ * in three shapes: a bare name, a `dot_expression` chain (leaf), or a
2695
+ * `bracket_expression` (generic instantiation → its `left` leaf). */
2696
+ calleeLeafNode(fn) {
2697
+ let cur = fn;
2698
+ let guard = 0;
2699
+ while (cur && guard++ < 16) {
2700
+ switch (cur.type) {
2701
+ case "identifier":
2702
+ case "accent_quoted":
2703
+ return cur;
2704
+ case "dot_expression": {
2705
+ const right = cur.childForFieldName("right");
2706
+ if (!right) return null;
2707
+ if (right.type === "identifier" || right.type === "accent_quoted") return right;
2708
+ cur = right;
2709
+ continue;
2710
+ }
2711
+ case "bracket_expression": {
2712
+ const left = cur.childForFieldName("left");
2713
+ if (!left) return null;
2714
+ cur = left;
2715
+ continue;
2716
+ }
2717
+ case "parenthesized": {
2718
+ const inner = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
2719
+ if (!inner) return null;
2720
+ cur = inner;
2721
+ continue;
2722
+ }
2723
+ default:
2724
+ return null;
2725
+ }
2726
+ }
2727
+ return null;
2728
+ }
2729
+ /** True when this identifier is the `right` field of a `dot_expression` — a
2730
+ * MEMBER read, which the dot branch owns. Without this the bare-identifier
2731
+ * branch claimed it first and the deduped edge was mislabelled. */
2732
+ isDotLeaf(node) {
2733
+ const p = node.parent;
2734
+ if (!p || p.type !== "dot_expression") return false;
2735
+ const right = p.childForFieldName("right");
2736
+ return !!right && right.startIndex === node.startIndex && right.endIndex === node.endIndex;
2737
+ }
2738
+ /** The member leaf of a `dot_expression` — its `right` field. */
2739
+ dotLeafNode(expr) {
2740
+ const right = expr.childForFieldName("right");
2741
+ if (right && (right.type === "identifier" || right.type === "accent_quoted")) return right;
2742
+ return null;
2743
+ }
2744
+ /** True when this node IS the callee subtree of a `call` / `dot_generic_call`
2745
+ * — the `calls` edge owns that span, so a `uses` edge would be a duplicate. */
2746
+ isCallCallee(node) {
2747
+ const p = node.parent;
2748
+ return !!p && (p.type === "call" || p.type === "dot_generic_call");
2749
+ }
2750
+ isCallCalleeSubtree(node) {
2751
+ let top = node;
2752
+ let p = node.parent;
2753
+ while (p && p.type !== "call" && p.type !== "dot_generic_call" && p.type !== "statement_list" && p.type !== "block") {
2754
+ top = p;
2755
+ p = p.parent;
2756
+ }
2757
+ if (!p || p.type !== "call" && p.type !== "dot_generic_call") return false;
2758
+ const fn = p.childForFieldName("function");
2759
+ if (!fn) return false;
2760
+ return top.startIndex >= fn.startIndex && top.endIndex <= fn.endIndex;
2761
+ }
2762
+ /**
2763
+ * True when the identifier names a declaration rather than reading one. The
2764
+ * grammar puts every declared name in exactly one of these shapes, so this is
2765
+ * a closed list and not a heuristic: `symbol_declaration` (fields, values,
2766
+ * for-loop variables, tuple destructuring), `type_symbol_declaration`
2767
+ * (types), `enum_field_declaration`, `variant_discriminator_declaration`,
2768
+ * and a `for` loop's `left` list.
2769
+ */
2770
+ /**
2771
+ * True for a CONCEPT's formal parameter: the `x` of `type Addable = concept x`.
2772
+ *
2773
+ * It is a binding, not a read, and it is the one formal parameter in Nim
2774
+ * written without any of the syntax the other binders use — no
2775
+ * `symbol_declaration` wrapper, no type annotation, nothing for
2776
+ * `isDeclarationName` to match. The read pass therefore published
2777
+ * `uses (global) -> x` at file scope.
2778
+ *
2779
+ * Found by the M12 ground truth, where it was the single false positive out of
2780
+ * 105 observed facts (precision 0.990). Only an independent audit could catch
2781
+ * it: the differential's oracle did not emit the edge either, so the lane
2782
+ * reported 0% unexplained with two implementations agreeing on something
2783
+ * neither should have published.
2784
+ */
2785
+ isConceptParameterPosition(node) {
2786
+ const parent = node.parent;
2787
+ if (!parent || parent.type !== "parameter_list") return false;
2788
+ return this.hasAncestorOfType(parent, "concept_declaration");
2789
+ }
2790
+ isDeclarationName(node) {
2791
+ const p = node.parent;
2792
+ if (!p) return false;
2793
+ switch (p.type) {
2794
+ case "symbol_declaration":
2795
+ case "type_symbol_declaration":
2796
+ case "enum_field_declaration":
2797
+ case "variant_discriminator_declaration":
2798
+ // `x*: int` — the export marker wraps the name, so the identifier's parent
2799
+ // is `exported_symbol` and the `symbol_declaration` case above never fires.
2800
+ case "exported_symbol":
2801
+ case "tuple_deconstruct_declaration":
2802
+ case "symbol_declaration_list":
2803
+ return true;
2804
+ default:
2805
+ return false;
2806
+ }
2807
+ }
2808
+ /**
2809
+ * True when the identifier sits in a TYPE position. Determined by the
2810
+ * grammar's own `type` / `return_type` / `inherits` / `refines` /
2811
+ * `generic_parameters` fields and span containment, never by an ancestry
2812
+ * guess — a type read is a declared-type fact, and emitting a `uses` edge for
2813
+ * it as well would double-count.
2814
+ */
2815
+ isInTypePosition(node) {
2816
+ let cur = node;
2817
+ let p = node.parent;
2818
+ let guard = 0;
2819
+ while (p && guard++ < 32) {
2820
+ if (p.type === "statement_list" || p.type === "block" || p.type === "source_file" || p.type === "do_block") return false;
2821
+ for (const f of TYPE_POSITION_FIELDS) {
2822
+ const field = p.childForFieldName(f);
2823
+ if (field && field.startIndex <= cur.startIndex && field.endIndex >= cur.endIndex) return true;
2824
+ }
2825
+ cur = p;
2826
+ p = p.parent;
2827
+ }
2828
+ return false;
2829
+ }
2830
+ /** True when the identifier is part of an `import`/`export`/`include` path —
2831
+ * a module name, never a value read. */
2832
+ isModulePathPosition(node) {
2833
+ let cur = node;
2834
+ let p = node.parent;
2835
+ let guard = 0;
2836
+ while (p && guard++ < 16) {
2837
+ if (p.type === "import_statement" || p.type === "export_statement" || p.type === "include_statement" || p.type === "import_from_statement") {
2838
+ return true;
2839
+ }
2840
+ if (p.type === "statement_list" || p.type === "source_file" || p.type === "block") return false;
2841
+ cur = p;
2842
+ p = p.parent;
2843
+ }
2844
+ return false;
2845
+ }
2846
+ /**
2847
+ * True for a LABEL: the name of a `block name:`, the operand of `break name` /
2848
+ * `continue name`, and the left side of a `colon_expression`. Nim has no
2849
+ * ternary, so `x: value` only ever names a field — of an object construction
2850
+ * (`Vec(x: 1)`) or of a variant branch — and never reads one.
2851
+ */
2852
+ isColonLabel(node) {
2853
+ const p = node.parent;
2854
+ if (!p || p.type !== "colon_expression") return false;
2855
+ const left = p.childForFieldName("left");
2856
+ return !!left && left.startIndex === node.startIndex && left.endIndex === node.endIndex;
2857
+ }
2858
+ /** True for a `block name:` label and the `break name` / `continue name`
2859
+ * operand — a goto target, not a read. */
2860
+ isLabelPosition(node) {
2861
+ const p = node.parent;
2862
+ if (!p) return false;
2863
+ if (p.type === "break_statement" || p.type === "continue_statement") return true;
2864
+ const label = p.childForFieldName?.("label");
2865
+ return !!label && label.startIndex === node.startIndex && label.endIndex === node.endIndex;
2866
+ }
2867
+ /** True for the right side of an `… as name` in an `except` clause — a
2868
+ * binding, not a read. */
2869
+ isExceptAliasPosition(node) {
2870
+ const p = node.parent;
2871
+ if (!p || p.type !== "infix_expression") return false;
2872
+ const op = p.childForFieldName("operator");
2873
+ if (!op || op.type !== "as") return false;
2874
+ const right = p.childForFieldName("right");
2875
+ return !!right && right.startIndex === node.startIndex && right.endIndex === node.endIndex;
2876
+ }
2877
+ /**
2878
+ * An assignment target is a WRITE, never a read. For `x = 1` the left field's
2879
+ * identifier is the target; for `a.x = 1` the dot leaf is; for `a[i] = 1` the
2880
+ * base and the index are both READS, so nothing is suppressed. The first pass
2881
+ * (non-member) covers `identifier`; the `member` pass covers the dot leaf.
2882
+ */
2883
+ isAssignmentTarget(node, member = false) {
2884
+ const p = node.parent;
2885
+ if (!p || p.type !== "assignment") return false;
2886
+ const left = p.childForFieldName("left");
2887
+ if (!left || left.startIndex > node.startIndex || left.endIndex < node.endIndex) return false;
2888
+ if (member) return this.isLeftmostLeafOf(p);
2889
+ return left.type === "identifier";
2890
+ }
2891
+ isLeftmostLeafOf(node) {
2892
+ let cur = node;
2893
+ for (; ; ) {
2894
+ const p = cur.parent;
2895
+ if (!p) return false;
2896
+ if (p.type === "dot_expression") {
2897
+ const left = p.childForFieldName("left");
2898
+ if (left && left.startIndex <= cur.startIndex && left.endIndex >= cur.endIndex) {
2899
+ cur = p;
2900
+ continue;
2901
+ }
2902
+ return false;
2903
+ }
2904
+ if (p.type === "bracket_expression") {
2905
+ const left = p.childForFieldName("left");
2906
+ if (left && left.startIndex <= cur.startIndex && left.endIndex >= cur.endIndex) {
2907
+ cur = p;
2908
+ continue;
2909
+ }
2910
+ return false;
2911
+ }
2912
+ if (p.type === "call" || p.type === "parenthesized") {
2913
+ const first = p.namedChildCount > 0 ? p.namedChild(0) : null;
2914
+ if (first && first.startIndex <= cur.startIndex && first.endIndex >= cur.endIndex) {
2915
+ cur = p;
2916
+ continue;
2917
+ }
2918
+ return false;
2919
+ }
2920
+ return false;
2921
+ }
2922
+ }
2923
+ /** True when the leftmost identifier of a chain is a module qualifier from
2924
+ * this file's imports — `os.getCurrentDir()` is an import-based fact, not a
2925
+ * local member read. */
2926
+ isImportQualified(node, qualifiers) {
2927
+ if (qualifiers.size === 0) return false;
2928
+ let cur = node;
2929
+ let guard = 0;
2930
+ while (guard++ < 16) {
2931
+ if (cur.type === "identifier" || cur.type === "accent_quoted") return qualifiers.has(cur.text);
2932
+ let next = null;
2933
+ for (const f of ["left", "function", "first_argument"]) {
2934
+ const fld = cur.childForFieldName(f);
2935
+ if (fld && fld.startIndex <= cur.startIndex && cur.endIndex <= fld.endIndex) {
2936
+ next = fld;
2937
+ break;
2938
+ }
2939
+ }
2940
+ if (!next && cur.type !== "dot_expression" && cur.type !== "bracket_expression" && cur.type !== "call") {
2941
+ const first = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
2942
+ if (first && first.startIndex <= cur.startIndex && first.endIndex >= cur.endIndex) next = first;
2943
+ }
2944
+ if (!next) return false;
2945
+ cur = next;
2946
+ }
2947
+ return false;
2948
+ }
2949
+ /**
2950
+ * True for `sink T` / `out T` / `var T` / `let T` written as a one-argument
2951
+ * call — the shape this grammar gives a modifier keyword that has no node of
2952
+ * its own (`doc/grammar.txt` spells the modifier inline in `paramList`, and
2953
+ * `var`/`out`/`ref`/`ptr`/`distinct` each got a `*_type` node; `sink` did not).
2954
+ * `PARAMETER_MODIFIERS` is a closed set of grammar keywords, so this is a token
2955
+ * comparison and never a text pattern.
2956
+ */
2957
+ isParameterModifierCall(node) {
2958
+ if (node.type !== "call") return false;
2959
+ const fn = node.childForFieldName("function");
2960
+ if (!fn || fn.type !== "identifier") return false;
2961
+ if (PARAMETER_MODIFIERS.indexOf(fn.text) === -1) return false;
2962
+ const args = this.firstChildOfType(node, "argument_list");
2963
+ return !!args && args.namedChildCount === 1;
2964
+ }
2965
+ /**
2966
+ * The type CONSTRUCTOR leaf of a type expression: the leftmost identifier of
2967
+ * the outermost `bracket_expression` / `call`, unwrapping the `ref`/`ptr`/
2968
+ * `var`/`out`/`distinct`/`type` modifiers. `seq[T]` → `seq`, `Table[K, V]` →
2969
+ * `Table`, `ref Node` → `Node`, `int` → `int`.
2970
+ */
2971
+ typeConstructorLeaf(te) {
2972
+ let cur = te;
2973
+ let guard = 0;
2974
+ while (guard++ < 16) {
2975
+ if (cur.type === "identifier" || cur.type === "accent_quoted") return cur;
2976
+ if (cur.type === "type_expression") {
2977
+ const inner = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
2978
+ if (!inner) return null;
2979
+ cur = inner;
2980
+ continue;
2981
+ }
2982
+ if (cur.type === "ref_type" || cur.type === "pointer_type" || cur.type === "out_type" || cur.type === "var_type" || cur.type === "distinct_type") {
2983
+ const inner = this.lastNamedChild(cur);
2984
+ if (!inner) return null;
2985
+ cur = inner;
2986
+ continue;
2987
+ }
2988
+ if (cur.type === "bracket_expression") {
2989
+ const left = cur.childForFieldName("left");
2990
+ if (!left) return null;
2991
+ cur = left;
2992
+ continue;
2993
+ }
2994
+ if (cur.type === "call") {
2995
+ const fn = cur.childForFieldName("function");
2996
+ if (!fn) return null;
2997
+ if (this.isParameterModifierCall(cur)) {
2998
+ const inner = this.firstChildOfType(cur, "argument_list");
2999
+ const only = inner && inner.namedChildCount === 1 ? inner.namedChild(0) : null;
3000
+ if (only) {
3001
+ cur = only;
3002
+ continue;
3003
+ }
3004
+ }
3005
+ cur = fn;
3006
+ continue;
3007
+ }
3008
+ if (cur.type === "dot_expression") {
3009
+ const right = cur.childForFieldName("right");
3010
+ if (!right) return null;
3011
+ cur = right;
3012
+ continue;
3013
+ }
3014
+ return null;
3015
+ }
3016
+ return null;
3017
+ }
3018
+ /** The type ARGUMENT nodes of a generic type expression — `T` in `seq[T]`,
3019
+ * `K` and `V` in `Table[K, V]`. */
3020
+ typeArgumentNodes(te) {
3021
+ let cur = te;
3022
+ let guard = 0;
3023
+ while (guard++ < 16) {
3024
+ if (cur.type === "type_expression") {
3025
+ const inner = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
3026
+ if (!inner) return [];
3027
+ cur = inner;
3028
+ continue;
3029
+ }
3030
+ if (cur.type === "ref_type" || cur.type === "pointer_type" || cur.type === "out_type" || cur.type === "var_type" || cur.type === "distinct_type") {
3031
+ const inner = this.lastNamedChild(cur);
3032
+ if (!inner) return [];
3033
+ cur = inner;
3034
+ continue;
3035
+ }
3036
+ if (cur.type === "bracket_expression") {
3037
+ const right = cur.childForFieldName("right");
3038
+ if (!right) return [];
3039
+ const out = [];
3040
+ for (let i = 0; i < right.namedChildCount; i++) {
3041
+ const c = right.namedChild(i);
3042
+ if (c) out.push(c);
3043
+ }
3044
+ return out;
3045
+ }
3046
+ if (cur.type === "call") {
3047
+ if (this.isParameterModifierCall(cur)) {
3048
+ const inner = this.firstChildOfType(cur, "argument_list");
3049
+ const only = inner && inner.namedChildCount === 1 ? inner.namedChild(0) : null;
3050
+ return only ? [only] : [];
3051
+ }
3052
+ const args = this.firstChildOfType(cur, "argument_list");
3053
+ if (!args) return [];
3054
+ const out = [];
3055
+ for (let i = 0; i < args.namedChildCount; i++) {
3056
+ const c = args.namedChild(i);
3057
+ if (c) out.push(c);
3058
+ }
3059
+ return out;
3060
+ }
3061
+ return [];
3062
+ }
3063
+ return [];
3064
+ }
3065
+ /** The leaf NAME of a `type_expression`, used for `nimInherits` and for the
3066
+ * method receiver type. */
3067
+ typeLeafName(te) {
3068
+ const leaf = this.typeConstructorLeaf(te);
3069
+ return leaf ? leaf.text : "";
3070
+ }
3071
+ /**
3072
+ * A `method`'s dispatch receiver: the first parameter's declared type, as the
3073
+ * OUTERMOST type name. `method speak(a: Animal)` → `Animal`;
3074
+ * `method speak(d: ref Dog)` → `Dog`; a container receiver
3075
+ * (`method first[T](a: seq[T])`) dispatches on the CONTAINER (`seq`), so the
3076
+ * type arguments are deliberately not part of it. Derived from the AST
3077
+ * (`parameters` → first `parameter_declaration` → `type` field), never by
3078
+ * re-parsing the parameter's text.
3079
+ */
3080
+ methodReceiverTypeLeaf(node) {
3081
+ const params = node.childForFieldName("parameters");
3082
+ if (!params) return "";
3083
+ for (let i = 0; i < params.namedChildCount; i++) {
3084
+ const p = params.namedChild(i);
3085
+ if (!p || p.type !== "parameter_declaration") continue;
3086
+ const t = p.childForFieldName("type");
3087
+ if (!t) return "";
3088
+ const leaf = this.typeConstructorLeaf(t);
3089
+ return leaf ? leaf.text : "";
3090
+ }
3091
+ return "";
3092
+ }
3093
+ // ═══════════════════════════════════════════════════════════════════════
3094
+ // 6. PURE HELPERS
3095
+ // ═══════════════════════════════════════════════════════════════════════
3096
+ pushChildren(node, stack, inCallable, enclosingCallable, docExample, objectKind, exampleBody) {
3097
+ const exampleStart = exampleBody ? exampleBody.startIndex : -1;
3098
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
3099
+ const c = node.namedChild(i);
3100
+ if (!c) continue;
3101
+ const inExample = exampleStart >= 0 && c.startIndex === exampleStart;
3102
+ stack.push({
3103
+ node: c,
3104
+ // Inside the example, `inCallable` is FALSE: the C-D1 rule suppressed
3105
+ // these because they sat inside a routine BODY, and the example is not
3106
+ // the body. Value declarations are suppressed separately, by `docExample`.
3107
+ inCallable: inExample ? false : inCallable,
3108
+ enclosingCallable,
3109
+ docExample: inExample ? true : docExample,
3110
+ objectKind
3111
+ });
3112
+ }
3113
+ }
3114
+ /**
3115
+ * A routine's children are NOT all its body, and reading them as if they were
3116
+ * dropped real declarations.
3117
+ *
3118
+ * The pinned grammar ABSORBS a `##` doc comment into the declaration node (see
3119
+ * `collectDocModulesAndComplexity` for the two placements and why reading only
3120
+ * the sibling form measured zero docstrings). So for
3121
+ *
3122
+ * proc `of`*[T, S](x: T, y: typedesc[S]): bool =
3123
+ * ## Checks if `x` is an instance of `y`.
3124
+ * runnableExamples:
3125
+ * type
3126
+ * Base = ref object of RootObj
3127
+ * Sub1 = ref object of Base
3128
+ *
3129
+ * the doc comment — and the `runnableExamples:` code the Nim COMPILER compiles
3130
+ * as its own unit — is a child of the `proc_declaration`, sibling to the `body`
3131
+ * field. Pushing every child with `inCallable: true` therefore marked
3132
+ * `Base` and `Sub1` as body-local and published neither.
3133
+ *
3134
+ * Measured cost over the corpus: 9 `object_declaration.inherits` occurrences on
3135
+ * the parse tree had no published owner symbol, every one of them inside a
3136
+ * `runnableExamples` block (nim/lib/system.nim Sub1/Sub2, lib/std/assertions.nim
3137
+ * MyError, tests/stdlib/trepr.nim TFoo, tests/stdlib/tjsonmacro.nim MsgChallenge,
3138
+ * tests/template/template_issues.nim `name`) — so `extends` lost the edges and
3139
+ * the graph lost the symbols.
3140
+ *
3141
+ * Only the `body` field carries `inCallable: true`, which is exactly the
3142
+ * distinction the C-D1 rule wanted: a `type` section inside a BODY is still
3143
+ * dropped, and a `type` section inside a DOC COMMENT is a declaration the
3144
+ * compiler type-checks.
3145
+ */
3146
+ pushCallableChildren(node, stack, enclosingCallable) {
3147
+ const body = node.childForFieldName("body");
3148
+ const selfName = this.callableNameOf(node);
3149
+ const bodyStart = body ? body.startIndex : -1;
3150
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
3151
+ const c = node.namedChild(i);
3152
+ if (!c) continue;
3153
+ const isBody = c.startIndex === bodyStart && bodyStart >= 0;
3154
+ stack.push({ node: c, inCallable: isBody, enclosingCallable: isBody ? selfName : enclosingCallable, docExample: false, objectKind: "" });
3155
+ }
3156
+ }
3157
+ /** The published NAME of a routine node, or "" when it has none. */
3158
+ callableNameOf(node) {
3159
+ const nameNode = node.childForFieldName("name");
3160
+ if (!nameNode) return "";
3161
+ return this.readNameNode(nameNode).name;
3162
+ }
3163
+ nodeRange(node) {
3164
+ return {
3165
+ startLine: node.startPosition.row + 1,
3166
+ startColumn: node.startPosition.column + 1,
3167
+ endLine: node.endPosition.row + 1,
3168
+ endColumn: node.endPosition.column + 1,
3169
+ startIndex: node.startIndex,
3170
+ endIndex: node.endIndex
3171
+ };
3172
+ }
3173
+ nodeText(node) {
3174
+ return node ? node.text : "";
3175
+ }
3176
+ snippet(text) {
3177
+ return text.length > 50 ? `${text.slice(0, 50)}\u2026` : text;
3178
+ }
3179
+ /** The last NAMED child. `ref`/`ptr`/`var`/`out`/`distinct` are anonymous
3180
+ * tokens, so a wrapper node's only named child is the type it wraps —
3181
+ * `namedChild(1)` is always null on one. */
3182
+ lastNamedChild(node) {
3183
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
3184
+ const c = node.namedChild(i);
3185
+ if (c) return c;
3186
+ }
3187
+ return null;
3188
+ }
3189
+ firstChildOfType(node, type) {
3190
+ for (let i = 0; i < node.namedChildCount; i++) {
3191
+ const c = node.namedChild(i);
3192
+ if (c && c.type === type) return c;
3193
+ }
3194
+ return null;
3195
+ }
3196
+ symbolNamesIn(node) {
3197
+ const out = [];
3198
+ if (node.type === "symbol_declaration_list") {
3199
+ for (let i = 0; i < node.namedChildCount; i++) {
3200
+ const d = node.namedChild(i);
3201
+ if (!d) continue;
3202
+ if (d.type === "symbol_declaration") {
3203
+ const n = d.childForFieldName("name");
3204
+ const info2 = n ? this.readNameNode(n) : null;
3205
+ if (info2?.name) out.push(info2.name);
3206
+ } else if (d.type === "tuple_deconstruct_declaration") {
3207
+ for (let j = 0; j < d.namedChildCount; j++) {
3208
+ const inner = d.namedChild(j);
3209
+ const info2 = inner ? this.readNameNode(inner) : null;
3210
+ if (info2?.name) out.push(info2.name);
3211
+ }
3212
+ }
3213
+ }
3214
+ return out;
3215
+ }
3216
+ const info = this.readNameNode(node);
3217
+ if (info.name) out.push(info.name);
3218
+ return out;
3219
+ }
3220
+ identifierTextsIn(node) {
3221
+ if (!node) return [];
3222
+ const out = [];
3223
+ const stack = [node];
3224
+ while (stack.length > 0) {
3225
+ const n = stack.pop();
3226
+ if (n.type === "identifier") {
3227
+ out.push(n.text);
3228
+ continue;
3229
+ }
3230
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
3231
+ const c = n.namedChild(i);
3232
+ if (c) stack.push(c);
3233
+ }
3234
+ }
3235
+ return out;
3236
+ }
3237
+ unquote(text) {
3238
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1);
3239
+ return text;
3240
+ }
3241
+ moduleSegments(path) {
3242
+ const parts = path.split("/");
3243
+ const out = [];
3244
+ for (const p of parts) {
3245
+ if (!p) continue;
3246
+ if (p === "." || p === "..") {
3247
+ out.push(p);
3248
+ continue;
3249
+ }
3250
+ out.push(p);
3251
+ }
3252
+ return out;
3253
+ }
3254
+ isDescendantOf(node, ancestor) {
3255
+ let p = node.parent;
3256
+ let guard = 0;
3257
+ while (p && guard++ < 32) {
3258
+ if (p.id === ancestor.id) return true;
3259
+ p = p.parent;
3260
+ }
3261
+ return false;
3262
+ }
3263
+ /**
3264
+ * Cyclomatic complexity over a body. Decision points are the grammar's own
3265
+ * branch/statement nodes (`if`, `when`, `case`, `while`, `for`, and every
3266
+ * `elif`/`else`/`of`/`except`/`finally` branch), plus the short-circuit
3267
+ * boolean operators. `and`/`or`/`xor`/`not` are ANONYMOUS tokens with their
3268
+ * own node types, so they are matched by token type, never by a substring of
3269
+ * the expression text.
3270
+ */
3271
+ nimCyclomaticComplexity(node) {
3272
+ let c = 1;
3273
+ const stack = [node];
3274
+ while (stack.length > 0) {
3275
+ const n = stack.pop();
3276
+ if (BRANCH_NODES.indexOf(n.type) !== -1) c++;
3277
+ if (n.type === "infix_expression") {
3278
+ const op = n.childForFieldName("operator");
3279
+ if (op && !op.isNamed && (op.type === "and" || op.type === "or" || op.type === "xor")) c++;
3280
+ }
3281
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
3282
+ const c2 = n.namedChild(i);
3283
+ if (c2) stack.push(c2);
3284
+ }
3285
+ }
3286
+ return c;
3287
+ }
3288
+ };
3289
+ var NIM_LITERAL_KEYWORDS = ["true", "false", "nil", "void", "auto"];
3290
+ var BACKSLASH = 92;
3291
+ var DOUBLE_QUOTE = 34;
3292
+ var SLASH = 47;
3293
+ var WHITESPACE_CODE_UNITS = /* @__PURE__ */ new Set([
3294
+ 9,
3295
+ 10,
3296
+ 11,
3297
+ 12,
3298
+ 13,
3299
+ // tab, LF, VT, FF, CR
3300
+ 32,
3301
+ // space
3302
+ 160,
3303
+ // no-break space
3304
+ 5760,
3305
+ // ogham space mark
3306
+ 8192,
3307
+ 8193,
3308
+ 8194,
3309
+ 8195,
3310
+ 8196,
3311
+ 8197,
3312
+ 8198,
3313
+ 8199,
3314
+ 8200,
3315
+ 8201,
3316
+ 8202,
3317
+ // en/em spaces + the rest of the U+2000 block
3318
+ 8232,
3319
+ 8233,
3320
+ // line/paragraph separator
3321
+ 8239,
3322
+ // narrow no-break space
3323
+ 8287,
3324
+ // medium mathematical space
3325
+ 12288,
3326
+ // ideographic space
3327
+ 65279
3328
+ // zero width no-break space
3329
+ ]);
3330
+ var isPathSpace = (code) => WHITESPACE_CODE_UNITS.has(code);
3331
+ var startsWithStdAt = (s, i) => s.charCodeAt(i) === 115 && // s
3332
+ s.charCodeAt(i + 1) === 116 && // t
3333
+ s.charCodeAt(i + 2) === 100;
3334
+ var TYPE_POSITION_FIELDS = ["type", "return_type", "inherits", "refines", "generic_parameters"];
3335
+ var BRANCH_NODES = [
3336
+ "if",
3337
+ "when",
3338
+ "case",
3339
+ "while",
3340
+ "for",
3341
+ "elif_branch",
3342
+ "else_branch",
3343
+ "of_branch",
3344
+ "except_branch",
3345
+ "finally_branch"
3346
+ ];
3347
+ var REAL_NIM_BUILTINS = [
3348
+ "echo",
3349
+ "assert",
3350
+ "new",
3351
+ "newSeq",
3352
+ "newString",
3353
+ "newFloat",
3354
+ "inc",
3355
+ "dec",
3356
+ "swap",
3357
+ "copyMem",
3358
+ "moveMem",
3359
+ "zeroMem",
3360
+ "getType",
3361
+ "typeof",
3362
+ "sizeof",
3363
+ "alignof",
3364
+ "isNil",
3365
+ "wasMoved",
3366
+ "incl",
3367
+ "excl",
3368
+ "high",
3369
+ "low",
3370
+ "ord",
3371
+ "result",
3372
+ "nil",
3373
+ "true",
3374
+ "false",
3375
+ "cast",
3376
+ "addr",
3377
+ "bind",
3378
+ "mixin",
3379
+ "static",
3380
+ "discard",
3381
+ "raise",
3382
+ "yield",
3383
+ "return",
3384
+ "break",
3385
+ "continue",
3386
+ "type",
3387
+ "var",
3388
+ "let",
3389
+ "const",
3390
+ "object",
3391
+ "enum",
3392
+ "tuple",
3393
+ "proc",
3394
+ "func",
3395
+ "method",
3396
+ "iterator",
3397
+ "macro",
3398
+ "template",
3399
+ "converter",
3400
+ "concept",
3401
+ "out",
3402
+ "ref",
3403
+ "ptr",
3404
+ "distinct",
3405
+ "range",
3406
+ "set",
3407
+ "seq",
3408
+ "array",
3409
+ "varargs",
3410
+ "pointer",
3411
+ "auto",
3412
+ "untyped",
3413
+ "typedesc",
3414
+ "void",
3415
+ "int",
3416
+ "int8",
3417
+ "int16",
3418
+ "int32",
3419
+ "int64",
3420
+ "uint",
3421
+ "uint8",
3422
+ "uint16",
3423
+ "uint32",
3424
+ "uint64",
3425
+ "float",
3426
+ "float32",
3427
+ "float64",
3428
+ "bool",
3429
+ "char",
3430
+ "string",
3431
+ "cstring",
3432
+ "byte",
3433
+ "ordinal",
3434
+ "system"
3435
+ ];
3436
+ var PARAMETER_MODIFIERS = ["sink", "out", "var", "let", "static"];
3437
+ var NIM_COMPILER_ROOTS = /* @__PURE__ */ new Set(["RootObj", "RootRef", "Object", "rootObj"]);
3438
+ var NIM_CONCEPT_SELF = "Self";
3439
+ var NIM_TYPE_POSITION_KEYWORDS = ["static", "type"];
3440
+ var NIM_BUILTIN_TYPES = [
3441
+ // integer / float / fixed-width
3442
+ "int",
3443
+ "int8",
3444
+ "int16",
3445
+ "int32",
3446
+ "int64",
3447
+ "uint",
3448
+ "uint8",
3449
+ "uint16",
3450
+ "uint32",
3451
+ "uint64",
3452
+ "float",
3453
+ "float32",
3454
+ "float64",
3455
+ "bool",
3456
+ "char",
3457
+ "byte",
3458
+ "string",
3459
+ "cstring",
3460
+ "void",
3461
+ "auto",
3462
+ "untyped",
3463
+ "typedesc",
3464
+ "ordinal",
3465
+ "range",
3466
+ // algebraic type classes the standard library defines
3467
+ "SomeInteger",
3468
+ "SomeFloat",
3469
+ "SomeNumber",
3470
+ "SomeOrdinal",
3471
+ "SomeOrdered",
3472
+ "Natural",
3473
+ "Positive",
3474
+ "Signed",
3475
+ "Unsigned",
3476
+ "Ordinal",
3477
+ "Ordered",
3478
+ "SomeComparable",
3479
+ "SomeCopyable",
3480
+ "SomeDefault",
3481
+ // containers (a declared-type reference to a container is not a dependency
3482
+ // on a user type; the type ARGUMENTS are collected separately)
3483
+ "seq",
3484
+ "array",
3485
+ "openArray",
3486
+ "set",
3487
+ "tuple",
3488
+ "varargs",
3489
+ "pointer",
3490
+ "Table",
3491
+ "OrderedTable",
3492
+ "CountTable",
3493
+ "HashSet",
3494
+ "OrderedSet",
3495
+ "CountSet",
3496
+ "touches",
3497
+ "isNil",
3498
+ "lent",
3499
+ // c-interop aliases
3500
+ "cpointer",
3501
+ "clong",
3502
+ "cint",
3503
+ "cshort",
3504
+ "cchar",
3505
+ "cschar",
3506
+ "cuchar",
3507
+ "csize",
3508
+ "cfloat",
3509
+ "cdouble",
3510
+ "clonglong",
3511
+ "culonglong",
3512
+ "cstringarray",
3513
+ // the compiler's own roots and the module every file implicitly imports
3514
+ "Object",
3515
+ "RootObj",
3516
+ "RootRef",
3517
+ "rootObj",
3518
+ "system",
3519
+ // common standard-library types that appear bare in annotations
3520
+ "File",
3521
+ "FileStream",
3522
+ "PFile",
3523
+ "PFileStream",
3524
+ "StringStream",
3525
+ "WideString",
3526
+ "charStream",
3527
+ "CString",
3528
+ "TEndian",
3529
+ "Endian"
3530
+ ];
3531
+ function isKeywordLike(name) {
3532
+ return KEYWORD_LIKE.indexOf(name) !== -1;
3533
+ }
3534
+ var KEYWORD_LIKE = [
3535
+ "addr",
3536
+ "and",
3537
+ "as",
3538
+ "bind",
3539
+ "block",
3540
+ "break",
3541
+ "case",
3542
+ "concept",
3543
+ "const",
3544
+ "continue",
3545
+ "converter",
3546
+ "defer",
3547
+ "discard",
3548
+ "distinct",
3549
+ "div",
3550
+ "do",
3551
+ "elif",
3552
+ "else",
3553
+ "end",
3554
+ "enum",
3555
+ "except",
3556
+ "export",
3557
+ "finally",
3558
+ "for",
3559
+ "from",
3560
+ "func",
3561
+ "if",
3562
+ "import",
3563
+ "in",
3564
+ "include",
3565
+ "interface",
3566
+ "is",
3567
+ "isnot",
3568
+ "iterator",
3569
+ "let",
3570
+ "macro",
3571
+ "method",
3572
+ "mixin",
3573
+ "mod",
3574
+ "nil",
3575
+ "not",
3576
+ "notin",
3577
+ "object",
3578
+ "of",
3579
+ "or",
3580
+ "out",
3581
+ "proc",
3582
+ "ptr",
3583
+ "raise",
3584
+ "ref",
3585
+ "return",
3586
+ "shl",
3587
+ "shr",
3588
+ "static",
3589
+ "template",
3590
+ "try",
3591
+ "tuple",
3592
+ "type",
3593
+ "using",
3594
+ "var",
3595
+ "when",
3596
+ "while",
3597
+ "xor",
3598
+ "yield"
3599
+ ];
3600
+ export {
3601
+ REAL_NIM_BUILTINS,
3602
+ RealNimAnalyzer
3603
+ };