@backbencher/brain 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- 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-3ZTOHHSW.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
|
+
};
|