@backbencher/brain 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -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-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -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-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.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-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.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,3470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fileClaimForTarget
|
|
3
|
+
} from "./chunk-EWMWK7WN.js";
|
|
4
|
+
import {
|
|
5
|
+
ASTEngine
|
|
6
|
+
} from "./chunk-V44PGPA6.js";
|
|
7
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
8
|
+
import "./chunk-SK6YVHFZ.js";
|
|
9
|
+
import "./chunk-2BSKD7O3.js";
|
|
10
|
+
import "./chunk-KAET47ZL.js";
|
|
11
|
+
import "./chunk-KCDECRJY.js";
|
|
12
|
+
import "./chunk-MLKGABMK.js";
|
|
13
|
+
|
|
14
|
+
// src/ast/real-clean-analyzer.ts
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
var LOCAL_SCOPE_NAME = "local";
|
|
17
|
+
var OPTIONAL_PARAM_NAME = "_";
|
|
18
|
+
function declarationKey(frame, owner, name) {
|
|
19
|
+
return `${frame ?? ""}|${owner ?? ""}::${name}`;
|
|
20
|
+
}
|
|
21
|
+
function stripWhitespace(text) {
|
|
22
|
+
let out = "";
|
|
23
|
+
for (const ch of text) if (ch !== " " && ch !== " " && ch !== "\n" && ch !== "\r") out += ch;
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function unparenthesizeText(text) {
|
|
27
|
+
let body = text;
|
|
28
|
+
if (body.startsWith("(")) body = body.slice(1);
|
|
29
|
+
if (body.endsWith(")")) body = body.slice(0, -1);
|
|
30
|
+
const inner = body.trim();
|
|
31
|
+
if (!inner) return null;
|
|
32
|
+
if (inner.includes(",") || inner.includes("{") || inner.includes("}")) return null;
|
|
33
|
+
return inner;
|
|
34
|
+
}
|
|
35
|
+
function unquoteText(text) {
|
|
36
|
+
if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1);
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
var RESERVED_DECLARATION_NAMES = /* @__PURE__ */ new Set([
|
|
40
|
+
"as",
|
|
41
|
+
"case",
|
|
42
|
+
"class",
|
|
43
|
+
"code",
|
|
44
|
+
"definition",
|
|
45
|
+
"derive",
|
|
46
|
+
"else",
|
|
47
|
+
"export",
|
|
48
|
+
"foreign",
|
|
49
|
+
"from",
|
|
50
|
+
"generic",
|
|
51
|
+
"if",
|
|
52
|
+
"implementation",
|
|
53
|
+
"import",
|
|
54
|
+
"in",
|
|
55
|
+
"infix",
|
|
56
|
+
"infixl",
|
|
57
|
+
"infixr",
|
|
58
|
+
"instance",
|
|
59
|
+
"let",
|
|
60
|
+
"module",
|
|
61
|
+
"of",
|
|
62
|
+
"qualified",
|
|
63
|
+
"special",
|
|
64
|
+
"system",
|
|
65
|
+
"then",
|
|
66
|
+
"where",
|
|
67
|
+
"with"
|
|
68
|
+
]);
|
|
69
|
+
function isReservedDeclarationName(name) {
|
|
70
|
+
return name !== null && name !== void 0 && RESERVED_DECLARATION_NAMES.has(name);
|
|
71
|
+
}
|
|
72
|
+
var OPERATOR_LEAF_TYPES = /* @__PURE__ */ new Set([
|
|
73
|
+
"operator",
|
|
74
|
+
"operator_add",
|
|
75
|
+
"operator_and",
|
|
76
|
+
"operator_compare",
|
|
77
|
+
"operator_cons",
|
|
78
|
+
"operator_cons_strict",
|
|
79
|
+
"operator_dot",
|
|
80
|
+
"operator_exp",
|
|
81
|
+
"operator_mul",
|
|
82
|
+
"operator_or",
|
|
83
|
+
"operator_pipe",
|
|
84
|
+
"monad_bind",
|
|
85
|
+
"backtick_operator",
|
|
86
|
+
"uniqueness_star",
|
|
87
|
+
"parenthesized_operator"
|
|
88
|
+
]);
|
|
89
|
+
var DERIVE_TARGET_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
90
|
+
"type_variable",
|
|
91
|
+
"constructor",
|
|
92
|
+
"dotted_type",
|
|
93
|
+
"type_application",
|
|
94
|
+
"type_paren",
|
|
95
|
+
"parenthesized_operator"
|
|
96
|
+
]);
|
|
97
|
+
var PATTERN_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
98
|
+
"constructor_pattern",
|
|
99
|
+
"list_pattern",
|
|
100
|
+
"tuple_pattern",
|
|
101
|
+
"record_pattern",
|
|
102
|
+
"paren_pattern",
|
|
103
|
+
"unboxed_pattern",
|
|
104
|
+
"cons_pattern",
|
|
105
|
+
"strict_binding_pattern",
|
|
106
|
+
"record_update_pattern"
|
|
107
|
+
]);
|
|
108
|
+
var CONSTRAINT_MARKER_TYPES = /* @__PURE__ */ new Set([
|
|
109
|
+
"number",
|
|
110
|
+
"integer",
|
|
111
|
+
"float",
|
|
112
|
+
"uniqueness_star",
|
|
113
|
+
"uniqueness_constraints"
|
|
114
|
+
]);
|
|
115
|
+
var INSTANCE_CONTAINER_SUFFIX = "::instance";
|
|
116
|
+
var CALLABLE_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
117
|
+
"function_declaration",
|
|
118
|
+
"operator_definition",
|
|
119
|
+
"generic_case_definition",
|
|
120
|
+
"macro_definition"
|
|
121
|
+
]);
|
|
122
|
+
var COMMENT_TYPES = /* @__PURE__ */ new Set(["line_comment", "block_comment"]);
|
|
123
|
+
var DECISION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
124
|
+
"case_expression",
|
|
125
|
+
"case_alternative",
|
|
126
|
+
"guard_equation",
|
|
127
|
+
"guard",
|
|
128
|
+
"if_expression",
|
|
129
|
+
"comprehension_qualifier",
|
|
130
|
+
"binary_expression"
|
|
131
|
+
]);
|
|
132
|
+
var CLEAN_STD_ENV_NAMES = /* @__PURE__ */ new Set([
|
|
133
|
+
"Applicative",
|
|
134
|
+
"Arith",
|
|
135
|
+
"Array",
|
|
136
|
+
"Bool",
|
|
137
|
+
"CONS",
|
|
138
|
+
"Char",
|
|
139
|
+
"ConsPos",
|
|
140
|
+
"DynamicTemp",
|
|
141
|
+
"EITHER",
|
|
142
|
+
"Enum",
|
|
143
|
+
"Eq",
|
|
144
|
+
"FIELD",
|
|
145
|
+
"File",
|
|
146
|
+
"FileEnv",
|
|
147
|
+
"FileSystem",
|
|
148
|
+
"Files",
|
|
149
|
+
"Functor",
|
|
150
|
+
"GenConsAssoc",
|
|
151
|
+
"GenConsPrio",
|
|
152
|
+
"GenType",
|
|
153
|
+
"GenericConsDescriptor",
|
|
154
|
+
"GenericDict",
|
|
155
|
+
"GenericDict0",
|
|
156
|
+
"GenericFieldDescriptor",
|
|
157
|
+
"GenericRecordDescriptor",
|
|
158
|
+
"GenericTypeDefDescriptor",
|
|
159
|
+
"IncDec",
|
|
160
|
+
"Int",
|
|
161
|
+
"List",
|
|
162
|
+
"Maybe",
|
|
163
|
+
"Monad",
|
|
164
|
+
"MultDiv",
|
|
165
|
+
"NormaliseState",
|
|
166
|
+
"Num",
|
|
167
|
+
"OBJECT",
|
|
168
|
+
"Option",
|
|
169
|
+
"Ord",
|
|
170
|
+
"PAIR",
|
|
171
|
+
"PlusMin",
|
|
172
|
+
"Prelude",
|
|
173
|
+
"RECORD",
|
|
174
|
+
"Real",
|
|
175
|
+
"Semigroup",
|
|
176
|
+
"Show",
|
|
177
|
+
"St",
|
|
178
|
+
"StdArray",
|
|
179
|
+
"StdBool",
|
|
180
|
+
"StdChar",
|
|
181
|
+
"StdCharList",
|
|
182
|
+
"StdClass",
|
|
183
|
+
"StdDebug",
|
|
184
|
+
"StdDynamic",
|
|
185
|
+
"StdEnum",
|
|
186
|
+
"StdEnv",
|
|
187
|
+
"StdFile",
|
|
188
|
+
"StdFunc",
|
|
189
|
+
"StdFunctions",
|
|
190
|
+
"StdGeneric",
|
|
191
|
+
"StdInt",
|
|
192
|
+
"StdList",
|
|
193
|
+
"StdMaybe",
|
|
194
|
+
"StdMisc",
|
|
195
|
+
"StdOrdList",
|
|
196
|
+
"StdOverloaded",
|
|
197
|
+
"StdOverloadedList",
|
|
198
|
+
"StdReal",
|
|
199
|
+
"StdStrictLists",
|
|
200
|
+
"StdString",
|
|
201
|
+
"StdTuple",
|
|
202
|
+
"String",
|
|
203
|
+
"TC_Bool",
|
|
204
|
+
"TC_Char",
|
|
205
|
+
"TC_Dynamic",
|
|
206
|
+
"TC_File",
|
|
207
|
+
"TC_Int",
|
|
208
|
+
"TC_Real",
|
|
209
|
+
"TC_World",
|
|
210
|
+
"TC__Arrow",
|
|
211
|
+
"TC__LazyArray",
|
|
212
|
+
"TC__LazyArrayP2",
|
|
213
|
+
"TC__List",
|
|
214
|
+
"TC__Maybe",
|
|
215
|
+
"TC__PackedArray",
|
|
216
|
+
"TC__PackedArrayP2",
|
|
217
|
+
"TC__STuple10",
|
|
218
|
+
"TC__STuple11",
|
|
219
|
+
"TC__STuple12",
|
|
220
|
+
"TC__STuple13",
|
|
221
|
+
"TC__STuple14",
|
|
222
|
+
"TC__STuple15",
|
|
223
|
+
"TC__STuple16",
|
|
224
|
+
"TC__STuple17",
|
|
225
|
+
"TC__STuple18",
|
|
226
|
+
"TC__STuple19",
|
|
227
|
+
"TC__STuple2",
|
|
228
|
+
"TC__STuple20",
|
|
229
|
+
"TC__STuple21",
|
|
230
|
+
"TC__STuple22",
|
|
231
|
+
"TC__STuple23",
|
|
232
|
+
"TC__STuple24",
|
|
233
|
+
"TC__STuple25",
|
|
234
|
+
"TC__STuple26",
|
|
235
|
+
"TC__STuple27",
|
|
236
|
+
"TC__STuple28",
|
|
237
|
+
"TC__STuple29",
|
|
238
|
+
"TC__STuple3",
|
|
239
|
+
"TC__STuple30",
|
|
240
|
+
"TC__STuple31",
|
|
241
|
+
"TC__STuple32",
|
|
242
|
+
"TC__STuple4",
|
|
243
|
+
"TC__STuple5",
|
|
244
|
+
"TC__STuple6",
|
|
245
|
+
"TC__STuple7",
|
|
246
|
+
"TC__STuple8",
|
|
247
|
+
"TC__STuple9",
|
|
248
|
+
"TC__StrictArray",
|
|
249
|
+
"TC__StrictArrayP2",
|
|
250
|
+
"TC__StrictList",
|
|
251
|
+
"TC__StrictMaybe",
|
|
252
|
+
"TC__StrictTailStrictList",
|
|
253
|
+
"TC__TailStrictList",
|
|
254
|
+
"TC__Tuple10",
|
|
255
|
+
"TC__Tuple11",
|
|
256
|
+
"TC__Tuple12",
|
|
257
|
+
"TC__Tuple13",
|
|
258
|
+
"TC__Tuple14",
|
|
259
|
+
"TC__Tuple15",
|
|
260
|
+
"TC__Tuple16",
|
|
261
|
+
"TC__Tuple17",
|
|
262
|
+
"TC__Tuple18",
|
|
263
|
+
"TC__Tuple19",
|
|
264
|
+
"TC__Tuple2",
|
|
265
|
+
"TC__Tuple20",
|
|
266
|
+
"TC__Tuple21",
|
|
267
|
+
"TC__Tuple22",
|
|
268
|
+
"TC__Tuple23",
|
|
269
|
+
"TC__Tuple24",
|
|
270
|
+
"TC__Tuple25",
|
|
271
|
+
"TC__Tuple26",
|
|
272
|
+
"TC__Tuple27",
|
|
273
|
+
"TC__Tuple28",
|
|
274
|
+
"TC__Tuple29",
|
|
275
|
+
"TC__Tuple3",
|
|
276
|
+
"TC__Tuple30",
|
|
277
|
+
"TC__Tuple31",
|
|
278
|
+
"TC__Tuple32",
|
|
279
|
+
"TC__Tuple4",
|
|
280
|
+
"TC__Tuple5",
|
|
281
|
+
"TC__Tuple6",
|
|
282
|
+
"TC__Tuple7",
|
|
283
|
+
"TC__Tuple8",
|
|
284
|
+
"TC__Tuple9",
|
|
285
|
+
"TC__UnboxedArray",
|
|
286
|
+
"TC__UnboxedArrayP2",
|
|
287
|
+
"TC__UnboxedList",
|
|
288
|
+
"TC__UnboxedMaybe",
|
|
289
|
+
"TC__UnboxedTailStrictList",
|
|
290
|
+
"TC__Unit",
|
|
291
|
+
"TypeCode",
|
|
292
|
+
"TypeCodeConstructor",
|
|
293
|
+
"UList",
|
|
294
|
+
"UMaybe",
|
|
295
|
+
"UNIT",
|
|
296
|
+
"UTSList",
|
|
297
|
+
"UnificationEnvironment",
|
|
298
|
+
"World",
|
|
299
|
+
"_SystemArray",
|
|
300
|
+
"_SystemDynamic",
|
|
301
|
+
"_SystemEnum",
|
|
302
|
+
"_SystemEnumStrict",
|
|
303
|
+
"_SystemStrictLists",
|
|
304
|
+
"_SystemStrictMaybes",
|
|
305
|
+
"_UnificationEnvironment"
|
|
306
|
+
]);
|
|
307
|
+
var RealCleanAnalyzer = class {
|
|
308
|
+
analyze(rootNode, fileContent, filePath, context) {
|
|
309
|
+
const ctx = {
|
|
310
|
+
symbols: [],
|
|
311
|
+
relationships: [],
|
|
312
|
+
imports: [],
|
|
313
|
+
exports: [],
|
|
314
|
+
errors: [],
|
|
315
|
+
patterns: {},
|
|
316
|
+
fixities: [],
|
|
317
|
+
declaredFunctions: /* @__PURE__ */ new Set(),
|
|
318
|
+
functionSymbolIndex: /* @__PURE__ */ Object.create(null),
|
|
319
|
+
signatures: /* @__PURE__ */ new Map(),
|
|
320
|
+
signaturesByNode: /* @__PURE__ */ new Map(),
|
|
321
|
+
localScopes: { callableScoped: /* @__PURE__ */ new Map(), blockLocals: /* @__PURE__ */ new Map(), frameLocals: /* @__PURE__ */ new Map() },
|
|
322
|
+
importAliases: /* @__PURE__ */ new Set(),
|
|
323
|
+
declaredTypeNames: /* @__PURE__ */ new Set(),
|
|
324
|
+
declaredTypeDefNames: /* @__PURE__ */ new Set(),
|
|
325
|
+
declaredFieldNames: /* @__PURE__ */ new Set(),
|
|
326
|
+
declaredConstructors: /* @__PURE__ */ new Set(),
|
|
327
|
+
declaredClassNames: /* @__PURE__ */ new Set(),
|
|
328
|
+
classMembers: /* @__PURE__ */ new Map(),
|
|
329
|
+
frameSymbolIndex: /* @__PURE__ */ new Map(),
|
|
330
|
+
currentModule: void 0,
|
|
331
|
+
currentFrame: void 0,
|
|
332
|
+
currentFrameIsLocal: false,
|
|
333
|
+
seenRels: /* @__PURE__ */ new Set(),
|
|
334
|
+
fileComplexity: 1,
|
|
335
|
+
declinedInstantiations: { count: 0, names: [] },
|
|
336
|
+
typeCoercions: { count: 0, names: [] },
|
|
337
|
+
operatorImports: { count: 0, names: [] },
|
|
338
|
+
foreignCodeImports: { count: 0, objects: [] },
|
|
339
|
+
detachedLocals: 0,
|
|
340
|
+
frameComplexity: /* @__PURE__ */ new Map()
|
|
341
|
+
};
|
|
342
|
+
const scopeStack = [];
|
|
343
|
+
const stack = [{ node: rootNode }];
|
|
344
|
+
this.collectNames(rootNode, ctx);
|
|
345
|
+
while (stack.length > 0) {
|
|
346
|
+
const entry = stack.pop();
|
|
347
|
+
if (entry.isExit) {
|
|
348
|
+
if (entry.restoresFrame) {
|
|
349
|
+
if (ctx.currentFrame !== void 0) {
|
|
350
|
+
const idx = ctx.frameSymbolIndex.get(ctx.currentFrame);
|
|
351
|
+
if (idx !== void 0) {
|
|
352
|
+
const sym = ctx.symbols[idx];
|
|
353
|
+
sym.complexity = Math.max(sym.complexity ?? 1, ctx.frameComplexity.get(ctx.currentFrame) ?? 1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
ctx.currentFrame = entry.restoreFrame;
|
|
357
|
+
ctx.currentFrameIsLocal = entry.restoreLocal === true;
|
|
358
|
+
}
|
|
359
|
+
if (scopeStack.length > 0 && scopeStack[scopeStack.length - 1].node === entry.node) scopeStack.pop();
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const node = entry.node;
|
|
363
|
+
if (node.isMissing) continue;
|
|
364
|
+
if (node.type === "ERROR") {
|
|
365
|
+
ctx.errors.push({
|
|
366
|
+
message: "Syntax Error",
|
|
367
|
+
range: {
|
|
368
|
+
startLine: node.startPosition.row + 1,
|
|
369
|
+
startColumn: node.startPosition.column,
|
|
370
|
+
endLine: node.endPosition.row + 1,
|
|
371
|
+
endColumn: node.endPosition.column,
|
|
372
|
+
startIndex: node.startIndex,
|
|
373
|
+
endIndex: node.endIndex
|
|
374
|
+
},
|
|
375
|
+
severity: "error"
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (DECISION_NODE_TYPES.has(node.type)) {
|
|
379
|
+
ctx.fileComplexity++;
|
|
380
|
+
if (ctx.currentFrame !== void 0) {
|
|
381
|
+
ctx.frameComplexity.set(ctx.currentFrame, (ctx.frameComplexity.get(ctx.currentFrame) ?? 1) + 1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
385
|
+
const inErrorRegion = entry.inError === true || node.type === "ERROR";
|
|
386
|
+
let frame = null;
|
|
387
|
+
let openedScope = false;
|
|
388
|
+
switch (node.type) {
|
|
389
|
+
case "module_declaration":
|
|
390
|
+
openedScope = this.enterModule(node, ctx, scopeStack);
|
|
391
|
+
break;
|
|
392
|
+
case "type_definition":
|
|
393
|
+
openedScope = this.processTypeDefinition(node, ctx, scopeStack);
|
|
394
|
+
break;
|
|
395
|
+
case "class_declaration":
|
|
396
|
+
openedScope = this.processClass(node, ctx, scopeStack);
|
|
397
|
+
break;
|
|
398
|
+
case "instance_declaration":
|
|
399
|
+
openedScope = this.processInstance(node, ctx, scopeStack);
|
|
400
|
+
break;
|
|
401
|
+
case "generic_declaration":
|
|
402
|
+
this.processGenericDeclaration(node, ctx, currentScope);
|
|
403
|
+
break;
|
|
404
|
+
case "derive_declaration":
|
|
405
|
+
this.processDeriveDeclaration(node, ctx);
|
|
406
|
+
break;
|
|
407
|
+
case "function_declaration":
|
|
408
|
+
case "operator_definition":
|
|
409
|
+
case "generic_case_definition":
|
|
410
|
+
frame = this.processCallableNode(node, ctx, currentScope, inErrorRegion);
|
|
411
|
+
break;
|
|
412
|
+
case "macro_definition":
|
|
413
|
+
frame = this.processMacro(node, ctx, currentScope);
|
|
414
|
+
break;
|
|
415
|
+
case "foreign_export":
|
|
416
|
+
this.processForeignExport(node, ctx, currentScope);
|
|
417
|
+
break;
|
|
418
|
+
case "fixity_declaration":
|
|
419
|
+
this.processFixityDeclaration(node, ctx);
|
|
420
|
+
break;
|
|
421
|
+
case "type_signature":
|
|
422
|
+
this.processTypeSignature(node, ctx, currentScope, inErrorRegion);
|
|
423
|
+
break;
|
|
424
|
+
case "where_block":
|
|
425
|
+
case "with_block":
|
|
426
|
+
case "let_expression":
|
|
427
|
+
scopeStack.push({
|
|
428
|
+
node,
|
|
429
|
+
name: LOCAL_SCOPE_NAME,
|
|
430
|
+
kind: ASTEngine.SymbolKind.Namespace,
|
|
431
|
+
locals: ctx.localScopes.blockLocals.get(node.id)
|
|
432
|
+
});
|
|
433
|
+
openedScope = true;
|
|
434
|
+
break;
|
|
435
|
+
case "special_block":
|
|
436
|
+
this.processSpecialBlock(node, ctx);
|
|
437
|
+
break;
|
|
438
|
+
case "application":
|
|
439
|
+
this.extractApplication(node, ctx, scopeStack);
|
|
440
|
+
break;
|
|
441
|
+
case "field_access":
|
|
442
|
+
this.emitFieldUse(node, ctx, "field_read");
|
|
443
|
+
break;
|
|
444
|
+
case "record_update":
|
|
445
|
+
this.extractRecordUpdate(node, ctx);
|
|
446
|
+
break;
|
|
447
|
+
case "record_expression":
|
|
448
|
+
this.extractRecordExpression(node, ctx);
|
|
449
|
+
break;
|
|
450
|
+
case "constructor_pattern":
|
|
451
|
+
this.extractConstructorPattern(node, ctx);
|
|
452
|
+
break;
|
|
453
|
+
case "lambda_expression":
|
|
454
|
+
ctx.patterns["Higher-Order Functions"] = true;
|
|
455
|
+
break;
|
|
456
|
+
case "list_comprehension":
|
|
457
|
+
case "array_comprehension":
|
|
458
|
+
ctx.patterns["List Comprehensions"] = true;
|
|
459
|
+
break;
|
|
460
|
+
case "case_expression":
|
|
461
|
+
case "case_alternative":
|
|
462
|
+
ctx.patterns["Pattern Matching"] = true;
|
|
463
|
+
break;
|
|
464
|
+
case "code_expression":
|
|
465
|
+
ctx.patterns["Inline Machine Code"] = true;
|
|
466
|
+
break;
|
|
467
|
+
case "unboxed_expression":
|
|
468
|
+
case "unboxed_pattern":
|
|
469
|
+
case "uniqueness_type":
|
|
470
|
+
case "uniqueness_star":
|
|
471
|
+
ctx.patterns["Uniqueness Types"] = true;
|
|
472
|
+
break;
|
|
473
|
+
case "type_definition_body":
|
|
474
|
+
ctx.patterns["Algebraic Data Types"] = true;
|
|
475
|
+
break;
|
|
476
|
+
case "record_definition":
|
|
477
|
+
ctx.patterns["Record Types"] = true;
|
|
478
|
+
break;
|
|
479
|
+
case "record_update_pattern":
|
|
480
|
+
case "update_field":
|
|
481
|
+
ctx.patterns["Record Updates"] = true;
|
|
482
|
+
break;
|
|
483
|
+
case "monad_bind":
|
|
484
|
+
ctx.patterns["Do Notation"] = true;
|
|
485
|
+
break;
|
|
486
|
+
case "range_operator":
|
|
487
|
+
ctx.patterns["Range Expressions"] = true;
|
|
488
|
+
break;
|
|
489
|
+
default:
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (frame) {
|
|
493
|
+
const previousFrame = ctx.currentFrame;
|
|
494
|
+
const previousLocal = ctx.currentFrameIsLocal;
|
|
495
|
+
ctx.currentFrame = previousFrame === void 0 ? frame.name : `${previousFrame}.${frame.name}`;
|
|
496
|
+
ctx.currentFrameIsLocal = frame.isLocal;
|
|
497
|
+
const frameIdx = ctx.functionSymbolIndex[frame.key];
|
|
498
|
+
if (frameIdx !== void 0) ctx.frameSymbolIndex.set(ctx.currentFrame, frameIdx);
|
|
499
|
+
scopeStack.push({
|
|
500
|
+
node,
|
|
501
|
+
name: frame.name,
|
|
502
|
+
kind: frame.kind,
|
|
503
|
+
locals: ctx.localScopes.frameLocals.get(node.id),
|
|
504
|
+
isFrame: true
|
|
505
|
+
});
|
|
506
|
+
stack.push({ node, isExit: true, restoresFrame: true, restoreFrame: previousFrame, restoreLocal: previousLocal });
|
|
507
|
+
} else if (openedScope) {
|
|
508
|
+
stack.push({ node, isExit: true });
|
|
509
|
+
}
|
|
510
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
511
|
+
const child = node.namedChild(i);
|
|
512
|
+
if (child) stack.push({ node: child, inError: inErrorRegion });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
this.attachFixities(ctx);
|
|
516
|
+
this.emitContainsEdges(ctx);
|
|
517
|
+
this.emitExports(ctx);
|
|
518
|
+
this.qualifyModuleLevelSymbols(ctx);
|
|
519
|
+
this.classifyEvidenceTier(ctx);
|
|
520
|
+
const claimRoot = typeof context?.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
521
|
+
if (claimRoot !== null) {
|
|
522
|
+
const root = path.resolve(claimRoot);
|
|
523
|
+
const fileRel = path.relative(root, filePath).split(path.sep).join("/");
|
|
524
|
+
const relDir = path.posix.dirname(fileRel);
|
|
525
|
+
const fileDir = relDir === "." ? "" : `${relDir}/`;
|
|
526
|
+
for (const r of ctx.relationships) {
|
|
527
|
+
if (r.type !== ASTEngine.RelationshipType.Imports) continue;
|
|
528
|
+
const md = r.metadata;
|
|
529
|
+
if (!md || typeof md.importProjectFile === "string") continue;
|
|
530
|
+
const moduleName = typeof md.module === "string" && md.module.length > 0 ? md.module : String(r.target ?? "").split(":")[0];
|
|
531
|
+
if (moduleName.length === 0) continue;
|
|
532
|
+
const modulePath = moduleName.split(".").join("/");
|
|
533
|
+
const rootCandidates = [`${modulePath}.icl`, `${modulePath}.dcl`];
|
|
534
|
+
const extraCandidates = fileDir.length > 0 ? [`${fileDir}${modulePath}.icl`, `${fileDir}${modulePath}.dcl`, ...rootCandidates] : rootCandidates;
|
|
535
|
+
const claim = fileClaimForTarget({
|
|
536
|
+
projectRoot: claimRoot,
|
|
537
|
+
filePath,
|
|
538
|
+
written: String(r.target ?? ""),
|
|
539
|
+
admittedExtensions: ["icl", "dcl"],
|
|
540
|
+
extraCandidates
|
|
541
|
+
});
|
|
542
|
+
if (claim === null) continue;
|
|
543
|
+
md.importProjectFile = claim;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
this.markStandardEnvironmentTargets(ctx);
|
|
547
|
+
const patterns = [];
|
|
548
|
+
for (const p in ctx.patterns) patterns.push(p);
|
|
549
|
+
return {
|
|
550
|
+
language: "clean",
|
|
551
|
+
symbols: ctx.symbols,
|
|
552
|
+
relationships: ctx.relationships,
|
|
553
|
+
imports: ctx.imports,
|
|
554
|
+
exports: ctx.exports,
|
|
555
|
+
errors: ctx.errors,
|
|
556
|
+
metadata: {
|
|
557
|
+
complexity: ctx.fileComplexity,
|
|
558
|
+
linesOfCode: fileContent.split("\n").length,
|
|
559
|
+
cyclomaticComplexity: ctx.fileComplexity,
|
|
560
|
+
patterns,
|
|
561
|
+
hasTypeInference: true,
|
|
562
|
+
analysisVersion: "12.0.0-clean-ast-complete",
|
|
563
|
+
// The price of the in-file resolution rule, stated in the output rather
|
|
564
|
+
// than inferred from a diff. See the field's declaration for the
|
|
565
|
+
// measurement behind it.
|
|
566
|
+
declinedInstantiations: ctx.declinedInstantiations.count,
|
|
567
|
+
declinedInstantiationNames: ctx.declinedInstantiations.names,
|
|
568
|
+
// Applying a declared type: a coercion, counted apart from the declines
|
|
569
|
+
// because the construct is known rather than unclassifiable.
|
|
570
|
+
typeCoercions: ctx.typeCoercions.count,
|
|
571
|
+
typeCoercionNames: ctx.typeCoercions.names,
|
|
572
|
+
// Imported operators: a real dependency the shared `<module>:<item>` edge
|
|
573
|
+
// form cannot express, so it is counted rather than emitted unresolvable.
|
|
574
|
+
operatorImports: ctx.operatorImports.count,
|
|
575
|
+
operatorImportNames: ctx.operatorImports.names,
|
|
576
|
+
// Foreign code imports: a real dependency the `<module>:<item>` edge form
|
|
577
|
+
// cannot express, because a linker input is not a module.
|
|
578
|
+
foreignCodeImports: ctx.foreignCodeImports.count,
|
|
579
|
+
foreignCodeObjects: ctx.foreignCodeImports.objects,
|
|
580
|
+
// Locals whose enclosing function the pinned grammar could not read, so the
|
|
581
|
+
// module owns them instead of a frame that does not exist.
|
|
582
|
+
detachedLocals: ctx.detachedLocals
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// PASS 1 — name tables
|
|
588
|
+
// ============================================================================
|
|
589
|
+
/**
|
|
590
|
+
* Imports, including the SELECTIVE item list.
|
|
591
|
+
*
|
|
592
|
+
* The old version pushed `items: []` unconditionally. Measured over the
|
|
593
|
+
* corpus: 367 of the 1,332 `import_declaration`s are `from M import …` with an
|
|
594
|
+
* item list — 943 items across 20 distinct item node types (`constructor` 541,
|
|
595
|
+
* `identifier` 188, `range_operator` 59, `parenthesized_operator` 45,
|
|
596
|
+
* `operator_compare` 30, `operator` 17, `record_subset` 11,
|
|
597
|
+
* `class_method_name` 10, `constructor_subset` 7, …). Every one of those names
|
|
598
|
+
* was invisible to the product: the import edge had to be `<module>:*` when the
|
|
599
|
+
* file actually said exactly which symbols it wanted, which is the difference
|
|
600
|
+
* between an edge that resolves and one that never can (see
|
|
601
|
+
* src/ast/import-resolution.ts — a target must be in the exporting file's
|
|
602
|
+
* export surface before it may be joined).
|
|
603
|
+
*/
|
|
604
|
+
collectImport(n, ctx, edgeSource, importEdges) {
|
|
605
|
+
{
|
|
606
|
+
{
|
|
607
|
+
const moduleNodes = this.fields(n, "module");
|
|
608
|
+
const aliasNode = n.childForFieldName("alias");
|
|
609
|
+
const alias = aliasNode ? this.assembleModuleName(aliasNode) : void 0;
|
|
610
|
+
let isQualified = false;
|
|
611
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
612
|
+
const c = n.child(i);
|
|
613
|
+
if (c && !c.isNamed && c.type === "qualified") {
|
|
614
|
+
isQualified = true;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (alias) ctx.importAliases.add(alias);
|
|
619
|
+
const items = this.importItems(n, moduleNodes);
|
|
620
|
+
for (const it of items) {
|
|
621
|
+
if (it.name.includes(":")) {
|
|
622
|
+
ctx.operatorImports.count++;
|
|
623
|
+
if (ctx.operatorImports.names.length < 25 && !ctx.operatorImports.names.includes(it.name)) {
|
|
624
|
+
ctx.operatorImports.names.push(it.name);
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const key = `${edgeSource}|${it.module}:${it.name}|${it.line}`;
|
|
629
|
+
if (importEdges.has(key)) continue;
|
|
630
|
+
importEdges.add(key);
|
|
631
|
+
this.pushRel(
|
|
632
|
+
ctx,
|
|
633
|
+
edgeSource,
|
|
634
|
+
`${it.module}:${it.name}`,
|
|
635
|
+
ASTEngine.RelationshipType.Imports,
|
|
636
|
+
1,
|
|
637
|
+
{
|
|
638
|
+
cleanSpecific: true,
|
|
639
|
+
module: it.module,
|
|
640
|
+
itemKind: it.kind,
|
|
641
|
+
isInstanceImport: it.kind === "instance",
|
|
642
|
+
isClassImport: it.kind === "class",
|
|
643
|
+
// ★ THE STDLIB CLAIM, emitted where the evidence exists (2026-10-28). The target of an
|
|
644
|
+
// import here is `${module}:${item}` (`StdEnv:map`) — a spelling no symbol can ever carry, so
|
|
645
|
+
// every one of the lane's 1,917 import edges read unresolved or unverifiable. The fleet's
|
|
646
|
+
// `moduleNamespace` claim is the mechanism for exactly this shape (python's `pathlib:Path`),
|
|
647
|
+
// and its evidence must be a CLOSED list, not a guess: `CLEAN_STD_ENV_NAMES` is the lane's own
|
|
648
|
+
// stdlib module set, measured, and the arbiter verifies the MODULE against the project's module
|
|
649
|
+
// space — so a project that DOES declare `StdEnv` REFUTES the claim. That direction is the one
|
|
650
|
+
// this lane must watch: `extFAIL` may not rise, and the arm is what says so.
|
|
651
|
+
// THE STDLIB CLAIM ONLY, AND A NON-STDLIB MODULE CARRIES NO CLAIM AT THIS SITE.
|
|
652
|
+
//
|
|
653
|
+
// ANY OTHER module (`Data.Error`, a corpus project's own `Clyde.Cocoa.tabview`) may be a
|
|
654
|
+
// project module or a dependency, and THE PROJECT'S FILE LIST DECIDES WHICH — but THIS SITE
|
|
655
|
+
// CANNOT SEE THAT LIST, so it must not state the claim. It used to: a flat
|
|
656
|
+
// `[Module/Sub.icl, Module/Sub.dcl]` was attached to every non-stdlib module, and the arm
|
|
657
|
+
// REFUTED **1,177 of the 1,615** claims it made (`extFAIL 1177`), because a Clean module's
|
|
658
|
+
// file is found on a SEARCH PATH (`Clyde` puts module `IdeState` at
|
|
659
|
+
// `cleantools/BatchBuild/IdeState.icl`, eastwood puts `Diagnostic` at
|
|
660
|
+
// `src/sharedUtil/Diagnostic.icl`) and the root-relative arithmetic below is only one of the
|
|
661
|
+
// spellings a loader tries. A file claim that a run can refute is a FALSE statement, not a
|
|
662
|
+
// conservative one: `fileClaimForTarget` refuses to emit an unconfirmed candidate for
|
|
663
|
+
// exactly this reason. PASS 3 owns the claim instead — it has `context.projectRoot`, it
|
|
664
|
+
// runs the shared guard over the importing file's own directory AND the project root, and
|
|
665
|
+
// it attaches a claim only when a candidate is CONFIRMED on disk (see the block there).
|
|
666
|
+
...CLEAN_STD_ENV_NAMES.has(it.module) ? { isExternal: true, externalReason: "module-namespace", externalSpecifier: it.module } : {}
|
|
667
|
+
},
|
|
668
|
+
it.line
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
for (const modNode of moduleNodes) {
|
|
672
|
+
if (!modNode) continue;
|
|
673
|
+
const modName = this.assembleModuleName(modNode);
|
|
674
|
+
const mine = items.filter((it) => it.module === modName);
|
|
675
|
+
const seenItem = /* @__PURE__ */ new Set();
|
|
676
|
+
const unique = mine.filter((it) => {
|
|
677
|
+
if (seenItem.has(it.name)) return false;
|
|
678
|
+
seenItem.add(it.name);
|
|
679
|
+
return true;
|
|
680
|
+
});
|
|
681
|
+
ctx.imports.push({
|
|
682
|
+
module: modName,
|
|
683
|
+
source: modName,
|
|
684
|
+
items: unique.map((it) => it.name),
|
|
685
|
+
isTypeOnly: false,
|
|
686
|
+
range: this.nodeToRange(n),
|
|
687
|
+
metadata: {
|
|
688
|
+
cleanSpecific: true,
|
|
689
|
+
isQualified,
|
|
690
|
+
alias,
|
|
691
|
+
isSelective: mine.length > 0,
|
|
692
|
+
itemKinds: mine.map((it) => it.kind),
|
|
693
|
+
// Which imported names are opened as a SUBSET (`::T(..)`,
|
|
694
|
+
// `::T{field}`). Recorded because the subset marker is not a name
|
|
695
|
+
// and so has no edge of its own, and "this import binds all of T's
|
|
696
|
+
// constructors" is a fact the product can use.
|
|
697
|
+
openItems: unique.filter((it) => it.opens).map((it) => `${it.name}${it.opens === "record" ? "{}" : "(..)"}`)
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
this.pushRel(
|
|
701
|
+
ctx,
|
|
702
|
+
ctx.currentModule || "",
|
|
703
|
+
modName,
|
|
704
|
+
ASTEngine.RelationshipType.References,
|
|
705
|
+
1,
|
|
706
|
+
{ cleanSpecific: true, isImportDependency: true },
|
|
707
|
+
this.lineOf(modNode)
|
|
708
|
+
);
|
|
709
|
+
if (modName.indexOf("StdEnv") !== -1) ctx.patterns["Standard Environment"] = true;
|
|
710
|
+
if (mine.length === 0) {
|
|
711
|
+
const key = `${edgeSource}|${modName}:*`;
|
|
712
|
+
if (!importEdges.has(key)) {
|
|
713
|
+
importEdges.add(key);
|
|
714
|
+
this.pushRel(
|
|
715
|
+
ctx,
|
|
716
|
+
edgeSource,
|
|
717
|
+
`${modName}:*`,
|
|
718
|
+
ASTEngine.RelationshipType.Imports,
|
|
719
|
+
1,
|
|
720
|
+
{
|
|
721
|
+
cleanSpecific: true,
|
|
722
|
+
module: modName,
|
|
723
|
+
isWildcard: true,
|
|
724
|
+
// THE WILDCARD FORM NEEDS THE SAME CLAIM AS THE ITEM FORM, and it was the gap: `import StdEnv`
|
|
725
|
+
// publishes `StdEnv:*`, and this site carried NO claim at all, so 52 `StdEnv:*` + 23 `StdString:*`
|
|
726
|
+
// rows read unresolved while the identical module on the item path was verified. MEASURED: the
|
|
727
|
+
// claim existed at ONE of the two emission sites (`grep -c module-namespace` = 1) and the
|
|
728
|
+
// unclaimed remainder was dominated by `StdEnv:*` (52), `StdString` (23), `StdBool` (21) — every
|
|
729
|
+
// one of them a module already in the closed `CLEAN_STD_ENV_NAMES` set.
|
|
730
|
+
// A NON-STDLIB module carries no file claim here — see the item site above for the 1,177
|
|
731
|
+
// refutations that measured why, and PASS 3 for where the claim is made instead.
|
|
732
|
+
...CLEAN_STD_ENV_NAMES.has(modName) ? { isExternal: true, externalReason: "module-namespace", externalSpecifier: modName } : {}
|
|
733
|
+
},
|
|
734
|
+
this.lineOf(modNode)
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* The item list of one import declaration, each item tagged by the keyword that
|
|
744
|
+
* introduces it.
|
|
745
|
+
*
|
|
746
|
+
* All four keyword forms exist in the corpus and each names a DIFFERENT thing,
|
|
747
|
+
* so the tag is load-bearing rather than cosmetic:
|
|
748
|
+
* `from M import ::NSApplication, sharedApplication` → `::` = public-only
|
|
749
|
+
* `from M import instance toString {#Char}` → an INSTANCE
|
|
750
|
+
* `from M import class C a` → a CLASS
|
|
751
|
+
* `import M => qualified join` → a QUALIFIED name
|
|
752
|
+
* `from M import ::GeneralSt{..}` → an open record
|
|
753
|
+
*/
|
|
754
|
+
importItems(node, moduleNodes) {
|
|
755
|
+
const out = [];
|
|
756
|
+
let currentModule = moduleNodes.length > 0 && moduleNodes[0] ? this.assembleModuleName(moduleNodes[0]) : "";
|
|
757
|
+
let keyword = "name";
|
|
758
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
759
|
+
const c = node.child(i);
|
|
760
|
+
if (!c) continue;
|
|
761
|
+
if (!c.isNamed) {
|
|
762
|
+
if (c.type === "instance") keyword = "instance";
|
|
763
|
+
else if (c.type === "class") keyword = "class";
|
|
764
|
+
else if (c.type === "qualified") keyword = "qualified";
|
|
765
|
+
else if (c.type === "::") keyword = "name";
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
let isModuleField = false;
|
|
769
|
+
for (let k = 0; k < node.childCount; k++) {
|
|
770
|
+
const field = node.fieldNameForChild(k);
|
|
771
|
+
if (field !== "module" && field !== "alias") continue;
|
|
772
|
+
const fc = node.child(k);
|
|
773
|
+
if (fc && fc.startIndex === c.startIndex) {
|
|
774
|
+
isModuleField = true;
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (isModuleField) {
|
|
779
|
+
if (c.type === "module_name" && moduleNodes.some((m) => m.startIndex === c.startIndex)) {
|
|
780
|
+
currentModule = this.assembleModuleName(c);
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (this.isOpenSubsetMarker(c)) {
|
|
785
|
+
if (out.length > 0) {
|
|
786
|
+
const last = out[out.length - 1];
|
|
787
|
+
last.opens = c.type === "record_subset" ? "record" : "constructor";
|
|
788
|
+
}
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
for (const described of this.describeImportItems(c, keyword)) {
|
|
792
|
+
out.push({ ...described, module: currentModule });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return out;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Is this import-item child an OPEN-SUBSET MARKER rather than a name?
|
|
799
|
+
*
|
|
800
|
+
* `constructor_subset`, `record_subset`, and the `parenthesized_operator` the
|
|
801
|
+
* grammar types for a `(..)` list — every one of which QUALIFIES the name in
|
|
802
|
+
* front of it. All three are real corpus node types (7, 11 and — as the `(..)`
|
|
803
|
+
* spelling — the bulk of the 512 `parenthesized_operator` sites).
|
|
804
|
+
*/
|
|
805
|
+
isOpenSubsetMarker(node) {
|
|
806
|
+
if (node.type === "record_subset") return true;
|
|
807
|
+
if (node.type === "constructor_subset") {
|
|
808
|
+
return this.firstNamedChildOfType(node, "constructor") === null;
|
|
809
|
+
}
|
|
810
|
+
if (node.type === "parenthesized_operator") return this.isDotsOnly(this.unparenthesize(node.text) ?? node.text);
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
/** Is every character a dot (and is there at least one)? `(..)` → true, `(==)` → false. */
|
|
814
|
+
isDotsOnly(text) {
|
|
815
|
+
const t = text.trim();
|
|
816
|
+
if (!t) return false;
|
|
817
|
+
for (const ch of t) if (ch !== ".") return false;
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
/** Name + kind for one import-item node, or null when it names nothing. */
|
|
821
|
+
describeImportItems(node, keyword) {
|
|
822
|
+
const line = this.lineOf(node);
|
|
823
|
+
if (!line) return [];
|
|
824
|
+
const kind = keyword === "name" ? "name" : keyword;
|
|
825
|
+
switch (node.type) {
|
|
826
|
+
case "identifier":
|
|
827
|
+
case "constructor":
|
|
828
|
+
return node.text ? [{ name: node.text, kind, line }] : [];
|
|
829
|
+
case "constructor_subset": {
|
|
830
|
+
const out = [];
|
|
831
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
832
|
+
const inner = node.namedChild(i);
|
|
833
|
+
if (!inner || inner.type !== "constructor" || !inner.text) continue;
|
|
834
|
+
out.push({ name: inner.text, kind, line });
|
|
835
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
}
|
|
838
|
+
case "class_method_name": {
|
|
839
|
+
const out = [];
|
|
840
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
841
|
+
const inner = node.namedChild(i);
|
|
842
|
+
if (!inner || !inner.text) continue;
|
|
843
|
+
out.push({ name: inner.text, kind, line });
|
|
844
|
+
}
|
|
845
|
+
return out.length ? out : node.text ? [{ name: node.text, kind, line }] : [];
|
|
846
|
+
}
|
|
847
|
+
case "backtick_operator": {
|
|
848
|
+
const bare = this.backtickOperatorName(node.text);
|
|
849
|
+
return bare ? [{ name: bare, kind, line }] : [];
|
|
850
|
+
}
|
|
851
|
+
default:
|
|
852
|
+
if (OPERATOR_LEAF_TYPES.has(node.type)) {
|
|
853
|
+
const name = node.text.trim();
|
|
854
|
+
return name ? [{ name, kind, line }] : [];
|
|
855
|
+
}
|
|
856
|
+
return [];
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/** `` `on` `` → `on`. A character filter, not a regex; an empty result is not a name. */
|
|
860
|
+
backtickOperatorName(text) {
|
|
861
|
+
let out = "";
|
|
862
|
+
for (const ch of text) if (ch !== "`") out += ch;
|
|
863
|
+
out = out.trim();
|
|
864
|
+
return out || null;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* THE NAME-COLLECTION WALK. One traversal builds every table the emitting walk
|
|
868
|
+
* needs: the module name, the imports and their item lists, the type signatures
|
|
869
|
+
* keyed by declaration identity, the declared type / class / constructor /
|
|
870
|
+
* field / callable name sets, and the lexical local scopes.
|
|
871
|
+
*
|
|
872
|
+
* It was four separate traversals (imports, signatures, declared names, local
|
|
873
|
+
* scopes) plus a fifth for the module header, so the analyzer visited the
|
|
874
|
+
* corpus's 331,079 nodes five times before emitting anything. All five need the
|
|
875
|
+
* same two pieces of context — the enclosing FRAME identity and the enclosing
|
|
876
|
+
* OWNER — so they share one walk and one tracker and the traversal count drops
|
|
877
|
+
* from six (five names + one emit) to two.
|
|
878
|
+
*
|
|
879
|
+
* The tracker is the same rule the emitting walk uses, in the same order, so the
|
|
880
|
+
* `declarationKey` a signature is stored under is the key the emitting walk
|
|
881
|
+
* looks it up with. Both call `declarationKey`; neither spells the identity.
|
|
882
|
+
*/
|
|
883
|
+
collectNames(root, ctx) {
|
|
884
|
+
const importEdges = /* @__PURE__ */ new Set();
|
|
885
|
+
const visit = (n, frameId, frame, owner) => {
|
|
886
|
+
let nextFrameId = frameId;
|
|
887
|
+
let nextFrame = frame;
|
|
888
|
+
let nextOwner = owner;
|
|
889
|
+
switch (n.type) {
|
|
890
|
+
case "module_declaration": {
|
|
891
|
+
const nameNode = n.childForFieldName("name");
|
|
892
|
+
if (nameNode) {
|
|
893
|
+
ctx.currentModule = this.assembleModuleName(nameNode);
|
|
894
|
+
nextOwner = ctx.currentModule;
|
|
895
|
+
}
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "import_declaration":
|
|
899
|
+
this.collectImport(n, ctx, ctx.currentModule ?? "", importEdges);
|
|
900
|
+
break;
|
|
901
|
+
case "foreign_code_import": {
|
|
902
|
+
ctx.foreignCodeImports.count++;
|
|
903
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
904
|
+
const item = n.namedChild(i);
|
|
905
|
+
if (!item || item.type !== "foreign_code_item") continue;
|
|
906
|
+
const obj = item.childForFieldName("object");
|
|
907
|
+
const name = obj ? unquoteText(obj.text) : void 0;
|
|
908
|
+
if (name && ctx.foreignCodeImports.objects.length < 25 && !ctx.foreignCodeImports.objects.includes(name)) {
|
|
909
|
+
ctx.foreignCodeImports.objects.push(name);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
case "type_definition": {
|
|
915
|
+
const nm = n.childForFieldName("name");
|
|
916
|
+
if (nm) {
|
|
917
|
+
ctx.declaredTypeNames.add(nm.text);
|
|
918
|
+
ctx.declaredTypeDefNames.add(nm.text);
|
|
919
|
+
}
|
|
920
|
+
const body = n.childForFieldName("body");
|
|
921
|
+
if (body) this.addTypeMention(body.childForFieldName("rhs") ?? body, ctx);
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
case "class_declaration": {
|
|
925
|
+
const nm = n.childForFieldName("name");
|
|
926
|
+
if (nm) {
|
|
927
|
+
ctx.declaredTypeNames.add(nm.text);
|
|
928
|
+
ctx.declaredClassNames.add(nm.text);
|
|
929
|
+
const members = /* @__PURE__ */ new Set();
|
|
930
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
931
|
+
const m = n.namedChild(i);
|
|
932
|
+
if (m?.type !== "class_member") continue;
|
|
933
|
+
const declared = this.memberDeclaredName(m);
|
|
934
|
+
if (declared) members.add(declared);
|
|
935
|
+
}
|
|
936
|
+
if (members.size > 0) {
|
|
937
|
+
ctx.classMembers.set(nm.text, members);
|
|
938
|
+
const bare = this.unparenthesize(nm.text);
|
|
939
|
+
if (bare !== null && bare !== nm.text) ctx.classMembers.set(bare, members);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
nextOwner = nm?.text ?? nextOwner;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
case "generic_declaration": {
|
|
946
|
+
const nm = n.childForFieldName("name");
|
|
947
|
+
if (nm) {
|
|
948
|
+
ctx.declaredTypeNames.add(nm.text);
|
|
949
|
+
ctx.declaredClassNames.add(nm.text);
|
|
950
|
+
}
|
|
951
|
+
nextOwner = nm?.text ?? nextOwner;
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case "instance_declaration":
|
|
955
|
+
for (const a of this.fields(n, "argument")) this.addTypeMention(a, ctx);
|
|
956
|
+
break;
|
|
957
|
+
case "generic_case_definition": {
|
|
958
|
+
const nm = n.childForFieldName("name");
|
|
959
|
+
if (nm) ctx.declaredFunctions.add(nm.text);
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
case "macro_definition": {
|
|
963
|
+
const name = this.normalizeParenthesized(n.childForFieldName("name"));
|
|
964
|
+
if (name) {
|
|
965
|
+
ctx.declaredFunctions.add(name);
|
|
966
|
+
ctx.declaredTypeNames.add(name);
|
|
967
|
+
}
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "foreign_export": {
|
|
971
|
+
const nm = n.childForFieldName("name");
|
|
972
|
+
if (nm) ctx.declaredFunctions.add(nm.text);
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
case "data_constructor": {
|
|
976
|
+
const nm = n.childForFieldName("name");
|
|
977
|
+
if (nm) {
|
|
978
|
+
ctx.declaredConstructors.add(nm.text);
|
|
979
|
+
ctx.declaredTypeNames.add(nm.text);
|
|
980
|
+
}
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
case "record_field": {
|
|
984
|
+
const nm = n.childForFieldName("name");
|
|
985
|
+
if (nm) {
|
|
986
|
+
ctx.declaredFieldNames.add(nm.text);
|
|
987
|
+
ctx.declaredTypeNames.add(nm.text);
|
|
988
|
+
}
|
|
989
|
+
const t = n.childForFieldName("type");
|
|
990
|
+
if (t) this.addTypeMention(t, ctx);
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
case "special_member": {
|
|
994
|
+
const t = n.childForFieldName("type");
|
|
995
|
+
if (t) this.addTypeMention(t, ctx);
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
case "type_signature": {
|
|
999
|
+
const name = this.declarationName(n);
|
|
1000
|
+
if (name) {
|
|
1001
|
+
ctx.declaredFunctions.add(name);
|
|
1002
|
+
const key = declarationKey(frame, owner, name);
|
|
1003
|
+
let info = ctx.signaturesByNode.get(n.id);
|
|
1004
|
+
if (!info) {
|
|
1005
|
+
info = this.readSignature(n, ctx);
|
|
1006
|
+
ctx.signaturesByNode.set(n.id, info);
|
|
1007
|
+
if (!ctx.signatures.has(key)) ctx.signatures.set(key, info);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
case "where_block":
|
|
1013
|
+
case "with_block":
|
|
1014
|
+
case "let_expression": {
|
|
1015
|
+
const map = this.localBlockLocals(n, ctx.localScopes, frame);
|
|
1016
|
+
if (map && map.size > 0) {
|
|
1017
|
+
ctx.localScopes.blockLocals.set(n.id, map);
|
|
1018
|
+
if (frameId >= 0) {
|
|
1019
|
+
const fm = ctx.localScopes.frameLocals.get(frameId) ?? /* @__PURE__ */ new Map();
|
|
1020
|
+
for (const [k, v] of map) fm.set(k, v);
|
|
1021
|
+
ctx.localScopes.frameLocals.set(frameId, fm);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
nextOwner = nextFrame;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
default:
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
const callableName = this.callableDeclaredName(n);
|
|
1031
|
+
if (callableName !== void 0) {
|
|
1032
|
+
if (n.type !== "type_definition") ctx.declaredFunctions.add(callableName);
|
|
1033
|
+
const scoped = nextFrame !== void 0 ? `${nextFrame}.${callableName}` : callableName;
|
|
1034
|
+
ctx.localScopes.callableScoped.set(n.id, scoped);
|
|
1035
|
+
nextFrameId = n.id;
|
|
1036
|
+
nextFrame = scoped;
|
|
1037
|
+
}
|
|
1038
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1039
|
+
const c = n.namedChild(i);
|
|
1040
|
+
if (c) visit(c, nextFrameId, nextFrame, nextOwner);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
visit(root, -1, void 0, void 0);
|
|
1044
|
+
}
|
|
1045
|
+
/** The binders a where/with/let block declares, as name -> scoped identity. */
|
|
1046
|
+
localBlockLocals(block, out, frameScoped) {
|
|
1047
|
+
let map = null;
|
|
1048
|
+
const consider = (defNode) => {
|
|
1049
|
+
const nm = this.callableDeclaredName(defNode);
|
|
1050
|
+
if (!nm) return;
|
|
1051
|
+
const scoped = frameScoped !== void 0 ? `${frameScoped}.${nm}` : nm;
|
|
1052
|
+
out.callableScoped.set(defNode.id, scoped);
|
|
1053
|
+
if (!map) map = /* @__PURE__ */ new Map();
|
|
1054
|
+
map.set(nm, scoped);
|
|
1055
|
+
};
|
|
1056
|
+
for (let i = 0; i < block.namedChildCount; i++) {
|
|
1057
|
+
const c = block.namedChild(i);
|
|
1058
|
+
if (!c) continue;
|
|
1059
|
+
if (c.type === "local_binding" || c.type === "record_binding") {
|
|
1060
|
+
const inner = c.namedChild(0);
|
|
1061
|
+
if (inner) consider(inner);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (c.type === "let_group") {
|
|
1065
|
+
for (const pat of this.fields(c, "pattern")) consider(pat);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
consider(c);
|
|
1069
|
+
}
|
|
1070
|
+
return map;
|
|
1071
|
+
}
|
|
1072
|
+
/** Read one `type_signature` into a `SignatureInfo`. */
|
|
1073
|
+
readSignature(node, ctx) {
|
|
1074
|
+
const info = {
|
|
1075
|
+
node,
|
|
1076
|
+
paramTypes: [],
|
|
1077
|
+
genericVars: /* @__PURE__ */ new Set(),
|
|
1078
|
+
typeRefs: [],
|
|
1079
|
+
contextClasses: [],
|
|
1080
|
+
specialTypes: [],
|
|
1081
|
+
hasCodeBody: false
|
|
1082
|
+
};
|
|
1083
|
+
const typeNode = node.childForFieldName("type");
|
|
1084
|
+
if (typeNode) this.readFunctionType(typeNode, info, ctx);
|
|
1085
|
+
const context = node.childForFieldName("context");
|
|
1086
|
+
if (context) this.readClassContext(context, info);
|
|
1087
|
+
const special = this.findChild(node, (n) => n.type === "special_block");
|
|
1088
|
+
if (special) {
|
|
1089
|
+
for (let i = 0; i < special.namedChildCount; i++) {
|
|
1090
|
+
const m = special.namedChild(i);
|
|
1091
|
+
if (!m || m.type !== "special_member") continue;
|
|
1092
|
+
const t = m.childForFieldName("type");
|
|
1093
|
+
if (t) for (const ref of this.typeRefsIn(t, ctx)) info.specialTypes.push(ref.name);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (node.childForFieldName("body")) info.hasCodeBody = true;
|
|
1097
|
+
const fixity = node.childForFieldName("fixity");
|
|
1098
|
+
if (fixity) {
|
|
1099
|
+
info.fixity = {
|
|
1100
|
+
associativity: fixity.namedChild(0)?.text,
|
|
1101
|
+
precedence: fixity.namedChild(1)?.text
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
return info;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* `[domain, arrow, result]` from a `type_function`, or a bare type when the
|
|
1108
|
+
* signature is not a function type at all (`f :: Int`).
|
|
1109
|
+
*/
|
|
1110
|
+
readFunctionType(typeNode, info, ctx) {
|
|
1111
|
+
if (typeNode.type !== "type_function") {
|
|
1112
|
+
info.returnType = typeNode.text;
|
|
1113
|
+
this.collectTypeRefs(typeNode, info, ctx, /* @__PURE__ */ new Set());
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const domain = typeNode.child(0);
|
|
1117
|
+
const result = typeNode.child(2);
|
|
1118
|
+
if (result) {
|
|
1119
|
+
info.returnType = result.text;
|
|
1120
|
+
this.collectTypeRefs(result, info, ctx, /* @__PURE__ */ new Set());
|
|
1121
|
+
}
|
|
1122
|
+
if (!domain) return;
|
|
1123
|
+
if (domain.type === "type_application") {
|
|
1124
|
+
for (let i = 0; i < domain.namedChildCount; i++) {
|
|
1125
|
+
const arg = domain.namedChild(i);
|
|
1126
|
+
if (!arg) continue;
|
|
1127
|
+
info.paramTypes.push(arg.text);
|
|
1128
|
+
this.collectTypeRefs(arg, info, ctx, /* @__PURE__ */ new Set());
|
|
1129
|
+
}
|
|
1130
|
+
} else {
|
|
1131
|
+
info.paramTypes.push(domain.text);
|
|
1132
|
+
this.collectTypeRefs(domain, info, ctx, /* @__PURE__ */ new Set());
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* The class names a `|` context constrains on, split at the `,` and `&` CHILD
|
|
1137
|
+
* nodes. Measured: `class_context` is `|` followed by a flat constraint list —
|
|
1138
|
+
* `| Enum, Ord, Num a` is ONE node with three named children and two `,`
|
|
1139
|
+
* tokens — so the split is a child walk, not a text split.
|
|
1140
|
+
*
|
|
1141
|
+
* **`&` is a second spelling of `,` and was not read.** The two forms produce
|
|
1142
|
+
* the SAME child shape with a different separator token, so this is the
|
|
1143
|
+
* two-spellings-one-handler case, not two constructs:
|
|
1144
|
+
*
|
|
1145
|
+
* | == a -> !| operator_compare type_variable
|
|
1146
|
+
* | == a & UList a -> !| operator_compare type_variable !& constructor type_variable
|
|
1147
|
+
*
|
|
1148
|
+
* (`clean-stdlib/StdOverloadedList.dcl:5,8`.) Reading only `,` published the
|
|
1149
|
+
* first conjunct and dropped the second. Measured over the corpus
|
|
1150
|
+
* (`validation/m14/fixtures/clean/ampersand-context-probe.mts`): 1,995
|
|
1151
|
+
* `class_context` nodes at WIDE carry only the three unnamed child types `|`,
|
|
1152
|
+
* `,` and `&`; 365 contain an `&`; and the conjuncts a `,`-only reader dropped
|
|
1153
|
+
* split **249 that would ADD a row** against **264 that would name a class
|
|
1154
|
+
* already published on that same line** — `| toReal a & toReal b & toReal c`
|
|
1155
|
+
* (Clyde/Clyde/timeprofile.icl:337) is the shape, and at TRACKED the split is
|
|
1156
|
+
* 62 against 47.
|
|
1157
|
+
*
|
|
1158
|
+
* The 264 are NOT a doubling, and the reason is `pushRel`'s own key rather than
|
|
1159
|
+
* a judgement made here: it collapses on `source|type|target|line`, so a repeated
|
|
1160
|
+
* conjunct cannot produce a second identical edge on one line. Measured as an A/B
|
|
1161
|
+
* on the TRACKED population, reverting only the `&` in this line
|
|
1162
|
+
* (`validation/m14/fixtures/clean/references-duplicate-probe.mts`):
|
|
1163
|
+
*
|
|
1164
|
+
* references edges distinct (source,target,line) groups with >1
|
|
1165
|
+
* `,` only 10,049 9,570 387
|
|
1166
|
+
* `,` and `&` 10,108 9,629 387
|
|
1167
|
+
*
|
|
1168
|
+
* Both columns of the third are identical, per discriminator, so the fix adds 59
|
|
1169
|
+
* edges and 59 NEW groups and changes the duplicate count by zero. The previous
|
|
1170
|
+
* round declined to ship this fix precisely because it could not show that, and
|
|
1171
|
+
* named the duplicate-vs-multiplicity question as the blocker; the question is
|
|
1172
|
+
* answered by a key that was already in the file. `249` is the WIDE count of rows
|
|
1173
|
+
* this adds, not a measured edge delta — WIDE is not re-measurable on this
|
|
1174
|
+
* machine, so only the TRACKED `+59` is a measurement of the output.
|
|
1175
|
+
*
|
|
1176
|
+
* `&&` is NOT handled: it is 0 of the 1,995 contexts, and handling an unobserved
|
|
1177
|
+
* token would be a claim no measurement supports.
|
|
1178
|
+
*
|
|
1179
|
+
* A constraint that names no class publishes nothing and emits no edge: 5 of
|
|
1180
|
+
* the 474 contexts in the corpus are `*` (a `uniqueness_star` leaf — the
|
|
1181
|
+
* unboxedness constraint `| * , toInt a`), and 469 name a class that does.
|
|
1182
|
+
*/
|
|
1183
|
+
readClassContext(context, info) {
|
|
1184
|
+
let expectName = true;
|
|
1185
|
+
for (let i = 0; i < context.childCount; i++) {
|
|
1186
|
+
const c = context.child(i);
|
|
1187
|
+
if (!c) continue;
|
|
1188
|
+
if (!c.isNamed) {
|
|
1189
|
+
if (c.type === "," || c.type === "&") expectName = true;
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
if (!expectName) continue;
|
|
1193
|
+
expectName = false;
|
|
1194
|
+
const name = this.constraintName(c);
|
|
1195
|
+
if (name) info.contextClasses.push(name);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* The class a single constraint names, or null when it names none. The
|
|
1200
|
+
* constraint node is a class name (`constructor` / `identifier`) or an operator
|
|
1201
|
+
* class (`operator_compare` for `==`, `operator_add` for `<+>`, …) — 287
|
|
1202
|
+
* constructors, 69 identifiers, 32 `operator_compare`, 10 `operator_add`, 2
|
|
1203
|
+
* `operator_mul`, 5 unnamed `*`.
|
|
1204
|
+
*/
|
|
1205
|
+
constraintName(c) {
|
|
1206
|
+
if (c.type === "constructor" || c.type === "identifier") return c.text;
|
|
1207
|
+
if (OPERATOR_LEAF_TYPES.has(c.type)) return c.text;
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Every TYPE NAME a type expression mentions, with type variables filtered out.
|
|
1212
|
+
* This is Clean's version of the TypeScript lane's `extractDeclaredTypeRefs`
|
|
1213
|
+
* (real-typescript-analyzer.ts:3144) and the single largest source of
|
|
1214
|
+
* relationships the lane did not have: 9,660 `strict_type` and 3,191
|
|
1215
|
+
* `type_application` sites over the corpus, every one of which names at least
|
|
1216
|
+
* one type.
|
|
1217
|
+
*
|
|
1218
|
+
* A `type_variable` is a BINDING, not a reference: `swap :: a a -> a` names no
|
|
1219
|
+
* type and binds two. That distinction is what makes `genericParameters`
|
|
1220
|
+
* correct and keeps `a` out of the reference graph.
|
|
1221
|
+
*
|
|
1222
|
+
* A module-qualified type is the one shape that needs context: `M.Map String
|
|
1223
|
+
* User` lexes as `constructor M`, `dotted_type .Map`, then the arguments, so
|
|
1224
|
+
* `M` is only a qualifier because it is a DECLARED IMPORT ALIAS of this file.
|
|
1225
|
+
* That is why `ctx` is in scope here: an alias set is a fact about the file,
|
|
1226
|
+
* and a qualifier is read from the alias set, not guessed from the spelling.
|
|
1227
|
+
*/
|
|
1228
|
+
collectTypeRefs(n, info, ctx, bound) {
|
|
1229
|
+
switch (n.type) {
|
|
1230
|
+
case "type_variable": {
|
|
1231
|
+
const inner = n.namedChild(0);
|
|
1232
|
+
if (inner?.text) {
|
|
1233
|
+
bound.add(inner.text);
|
|
1234
|
+
info.genericVars.add(inner.text);
|
|
1235
|
+
}
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
case "unique_var_type": {
|
|
1239
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1240
|
+
const c = n.namedChild(i);
|
|
1241
|
+
if (!c) continue;
|
|
1242
|
+
if (c.type === "type_variable") {
|
|
1243
|
+
const inner = c.namedChild(0);
|
|
1244
|
+
if (inner?.text) {
|
|
1245
|
+
bound.add(inner.text);
|
|
1246
|
+
info.genericVars.add(inner.text);
|
|
1247
|
+
}
|
|
1248
|
+
} else if (c.startIndex > n.startIndex) {
|
|
1249
|
+
this.collectTypeRefs(c, info, ctx, bound);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
case "constructor": {
|
|
1255
|
+
this.addTypeRef(info, n.text, n);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
case "single_quoted_name": {
|
|
1259
|
+
const target = this.qualifiedTarget(n.text);
|
|
1260
|
+
if (target) this.addTypeRef(info, target, n);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
case "dotted_type": {
|
|
1264
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1265
|
+
const c = n.namedChild(i);
|
|
1266
|
+
if (c) this.collectTypeRefs(c, info, ctx, bound);
|
|
1267
|
+
}
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
case "type_paren":
|
|
1271
|
+
case "strict_type":
|
|
1272
|
+
case "uniqueness_type":
|
|
1273
|
+
case "list_type":
|
|
1274
|
+
case "array_type":
|
|
1275
|
+
case "tuple_type":
|
|
1276
|
+
case "question_type":
|
|
1277
|
+
case "builtin_question_type":
|
|
1278
|
+
case "type_application": {
|
|
1279
|
+
const head = n.namedChild(0);
|
|
1280
|
+
if (head && head.type === "constructor" && ctx.importAliases.has(head.text)) {
|
|
1281
|
+
const second = n.namedChild(1);
|
|
1282
|
+
if (second) {
|
|
1283
|
+
const leafText = second.type === "dotted_type" ? stripLeadingDot(second.text) : second.text;
|
|
1284
|
+
if (leafText && second.type !== "type_variable") {
|
|
1285
|
+
this.addTypeRef(info, `${head.text}.${leafText}`, second);
|
|
1286
|
+
}
|
|
1287
|
+
for (let i = 2; i < n.namedChildCount; i++) {
|
|
1288
|
+
const c = n.namedChild(i);
|
|
1289
|
+
if (c) this.collectTypeRefs(c, info, ctx, bound);
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1295
|
+
const c = n.namedChild(i);
|
|
1296
|
+
if (c) this.collectTypeRefs(c, info, ctx, bound);
|
|
1297
|
+
}
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
default: {
|
|
1301
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1302
|
+
const c = n.namedChild(i);
|
|
1303
|
+
if (c) this.collectTypeRefs(c, info, ctx, bound);
|
|
1304
|
+
}
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
addTypeRef(info, name, node) {
|
|
1310
|
+
if (info.typeRefs.some((r) => r.name === name)) return;
|
|
1311
|
+
info.typeRefs.push({ name, node });
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Every bare type name a type expression mentions, with the node that spells
|
|
1315
|
+
* it. One reader (`collectTypeRefs`) and one rule, so the field path and the
|
|
1316
|
+
* signature path cannot drift — the drift is exactly what FIX B measured.
|
|
1317
|
+
*/
|
|
1318
|
+
typeRefsIn(typeNode, ctx) {
|
|
1319
|
+
const info = {
|
|
1320
|
+
node: typeNode,
|
|
1321
|
+
paramTypes: [],
|
|
1322
|
+
genericVars: /* @__PURE__ */ new Set(),
|
|
1323
|
+
typeRefs: [],
|
|
1324
|
+
contextClasses: [],
|
|
1325
|
+
specialTypes: [],
|
|
1326
|
+
hasCodeBody: false
|
|
1327
|
+
};
|
|
1328
|
+
this.collectTypeRefs(typeNode, info, ctx, /* @__PURE__ */ new Set());
|
|
1329
|
+
return info.typeRefs;
|
|
1330
|
+
}
|
|
1331
|
+
/** Every constructor named anywhere inside a type expression. */
|
|
1332
|
+
addTypeMention(n, ctx) {
|
|
1333
|
+
if (n.type === "type_variable") return;
|
|
1334
|
+
if (n.type === "constructor") {
|
|
1335
|
+
ctx.declaredTypeNames.add(n.text);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (n.type === "dotted_type") {
|
|
1339
|
+
const inner = n.namedChild(0);
|
|
1340
|
+
if (inner?.type === "constructor") ctx.declaredTypeNames.add(inner.text);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1344
|
+
const c = n.namedChild(i);
|
|
1345
|
+
if (c) this.addTypeMention(c, ctx);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/** The declared name of a callable node, or undefined when it declares none. */
|
|
1349
|
+
callableDeclaredName(node) {
|
|
1350
|
+
const declared = this.rawCallableDeclaredName(node);
|
|
1351
|
+
return isReservedDeclarationName(declared) ? void 0 : declared;
|
|
1352
|
+
}
|
|
1353
|
+
rawCallableDeclaredName(node) {
|
|
1354
|
+
switch (node.type) {
|
|
1355
|
+
case "function_declaration":
|
|
1356
|
+
return this.declaredName(node)?.text ?? void 0;
|
|
1357
|
+
case "operator_definition":
|
|
1358
|
+
return this.operatorDefinitionName(node) ?? void 0;
|
|
1359
|
+
case "generic_case_definition":
|
|
1360
|
+
return node.childForFieldName("name")?.text ?? void 0;
|
|
1361
|
+
case "type_signature":
|
|
1362
|
+
return this.declarationName(node) ?? void 0;
|
|
1363
|
+
case "macro_definition":
|
|
1364
|
+
return this.normalizeParenthesized(node.childForFieldName("name")) ?? void 0;
|
|
1365
|
+
case "type_definition":
|
|
1366
|
+
return node.childForFieldName("name")?.text ?? void 0;
|
|
1367
|
+
default:
|
|
1368
|
+
return void 0;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* A declaration the pinned grammar produced for TEXT THAT IS NOT A DECLARATION: its
|
|
1373
|
+
* declared name is a RESERVED WORD, re-lexed as an identifier by recovery (a `where`
|
|
1374
|
+
* opening a block in an already-damaged region becomes a top-level
|
|
1375
|
+
* `function_declaration` named `where`). The node is not a function — but its NAME is
|
|
1376
|
+
* a name the file writes at a declaration head, and F14's C1 (M60-C) requires every
|
|
1377
|
+
* declared name to be published somewhere in its file.
|
|
1378
|
+
*
|
|
1379
|
+
* THE ROW IS SURFACED, THE FRAME IS NOT. `visibility: "local"` keeps it out of the
|
|
1380
|
+
* export surface (`emitExports` skips local rows) and out of the module-qualified
|
|
1381
|
+
* identity (`qualifyModuleLevelSymbols` skips them), while `metadata.parseDamage`
|
|
1382
|
+
* says what the row is. Returning no `FrameSpec` is the load-bearing half: attributing
|
|
1383
|
+
* the applications BENEATH such a node to it is the `instantiates -> where -> XOffs`
|
|
1384
|
+
* fabrication the reserved-name decline existed for, and this change does not weaken
|
|
1385
|
+
* that rule — it only stops the NAME from being dropped along with the frame.
|
|
1386
|
+
*
|
|
1387
|
+
* Deliberately NOT indexed in `functionSymbolIndex`: a later real definition of the
|
|
1388
|
+
* same name must not merge into a damage row.
|
|
1389
|
+
*/
|
|
1390
|
+
publishParseDamageSymbol(node, name, ctx, currentScope) {
|
|
1391
|
+
const isLocal = this.isLocalScope(currentScope);
|
|
1392
|
+
ctx.symbols.push({
|
|
1393
|
+
name,
|
|
1394
|
+
kind: this.callableKind(currentScope, isLocal),
|
|
1395
|
+
range: this.nodeToRange(node),
|
|
1396
|
+
scope: currentScope?.name,
|
|
1397
|
+
ownerName: isLocal ? this.localOwner(ctx) : currentScope?.name || ctx.currentModule,
|
|
1398
|
+
ownerKind: isLocal ? ASTEngine.SymbolKind.Function : currentScope?.kind,
|
|
1399
|
+
visibility: "local",
|
|
1400
|
+
metadata: { cleanSpecific: true, parseDamage: true, isReservedDeclarationName: true }
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* The row a callable DEFINITION may merge into, or `undefined` when it must publish
|
|
1405
|
+
* one of its own.
|
|
1406
|
+
*
|
|
1407
|
+
* `declarationKey` is shared on purpose by the three merges that are real —
|
|
1408
|
+
* signature↔definition, clause↔clause, `foreign export`↔definition — so the KEY
|
|
1409
|
+
* cannot be what discriminates them; the KIND can. A MACRO is a different entity
|
|
1410
|
+
* from a callable definition even when the two spell the same name.
|
|
1411
|
+
*
|
|
1412
|
+
* MEASURED (F14 C1, `Clyde/cleantools/Pm/PmParse.icl`): the file defines the macro
|
|
1413
|
+
* `Special c :== …` at 1222, and the pinned grammar lifts a case alternative to a
|
|
1414
|
+
* top-level `function_declaration` named `Special` at 1333. The keyed merge treated
|
|
1415
|
+
* the second as a second clause of the macro and published only the macro row, so
|
|
1416
|
+
* the declaration's name was never published at all. That is a RULE defect — a
|
|
1417
|
+
* `function_declaration` may not merge into a macro row — not a name to suppress.
|
|
1418
|
+
*/
|
|
1419
|
+
callableMergeIndex(key, ctx) {
|
|
1420
|
+
const idx = ctx.functionSymbolIndex[key];
|
|
1421
|
+
if (idx === void 0) return void 0;
|
|
1422
|
+
return ctx.symbols[idx]?.kind === ASTEngine.SymbolKind.Macro ? void 0 : idx;
|
|
1423
|
+
}
|
|
1424
|
+
// ============================================================================
|
|
1425
|
+
// PASS 2 — declaration handlers
|
|
1426
|
+
// ============================================================================
|
|
1427
|
+
enterModule(node, ctx, scopeStack) {
|
|
1428
|
+
const nameNode = node.childForFieldName("name");
|
|
1429
|
+
const name = nameNode ? this.assembleModuleName(nameNode) : ctx.currentModule;
|
|
1430
|
+
if (!name) return false;
|
|
1431
|
+
ctx.currentModule = name;
|
|
1432
|
+
let moduleKind = "module";
|
|
1433
|
+
const first = node.child(0);
|
|
1434
|
+
if (first && !first.isNamed && (first.type === "definition" || first.type === "implementation")) {
|
|
1435
|
+
moduleKind = first.type;
|
|
1436
|
+
}
|
|
1437
|
+
ctx.symbols.push({
|
|
1438
|
+
name,
|
|
1439
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
1440
|
+
range: this.nodeToRange(node),
|
|
1441
|
+
visibility: "public",
|
|
1442
|
+
metadata: { cleanSpecific: true, moduleKind }
|
|
1443
|
+
});
|
|
1444
|
+
scopeStack.push({ node, name, kind: ASTEngine.SymbolKind.Module });
|
|
1445
|
+
return true;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* A type declaration. The pinned grammar has ONE node type for all six Clean
|
|
1449
|
+
* type forms, distinguished by the `type_definition_body.rhs` node type:
|
|
1450
|
+
* synonym (`:==`), abstract (no rhs), record (`= {…}`), ADT (`= C | D`),
|
|
1451
|
+
* enumerated (`= A | B`) and quantified ADT (`= E a | C !a`). Measured on the
|
|
1452
|
+
* 503 `type_definition_body` sites: 255 `data_constructors`, 114
|
|
1453
|
+
* `record_definition`, 49 `type_application`, 3 `tuple_type`, 6 `array_type`,
|
|
1454
|
+
* 3 `list_type`, 1 `type_paren`, 1 `unit_type` and 71 bare `constructor`
|
|
1455
|
+
* (synonym / enumerated).
|
|
1456
|
+
*/
|
|
1457
|
+
/**
|
|
1458
|
+
* Is this `type_definition` the PARSER'S RECOVERY OF A SIGNATURE rather than a
|
|
1459
|
+
* declaration? A Clean signature such as `widByPtr :: !WID -> Bool` is not a
|
|
1460
|
+
* `type_signature` to the pinned grammar: it comes back as a `type_definition` with no
|
|
1461
|
+
* `body`, no `rhs`, a `name` field holding the FIRST ARGUMENT TYPE, and text that stops
|
|
1462
|
+
* there (`:: !WID`). Publishing that row puts a fabricated `type` in the symbol set and
|
|
1463
|
+
* makes every edge that mentions the argument type point at it.
|
|
1464
|
+
*
|
|
1465
|
+
* MEASURED over the 2,179-file WIDE manifest (`validation/m14/measurements/clean/
|
|
1466
|
+
* type-definition-recovery-2179.txt`, regenerable with
|
|
1467
|
+
* `npx tsx .tmp-probe/tdef-rule.mts`), production wasm, node shapes only:
|
|
1468
|
+
*
|
|
1469
|
+
* type_definition with a body or rhs 3,930
|
|
1470
|
+
* with NEITHER 447 (10.21% of all type_definitions)
|
|
1471
|
+
*
|
|
1472
|
+
* Of the 447, 74 carry a uniqueness marker (`!`, `#`) or an `ERROR` as the child
|
|
1473
|
+
* immediately after the `::`, and reading all 74 sites shows every one of them is a
|
|
1474
|
+
* signature. The other 373 are real declarations the rule must not touch: 293 `:: ErrPrefs`
|
|
1475
|
+
* (an abstract type in a `.dcl`), 32 `:: Conversions a`, 25 `:: *TTY` (an abstract unique
|
|
1476
|
+
* class) and 23 with a trailing `;` or a body the grammar filed under `uniqueness_type`.
|
|
1477
|
+
*
|
|
1478
|
+
* 74 IS NOT THE WITHDRAWABLE SET, and the reason is the trap this function exists to
|
|
1479
|
+
* avoid. `eastwood/test/suite-default/GoToModule1.dcl:89` is `:: !*AbstractTypeSynonym`
|
|
1480
|
+
* and `:92` is `:: !*EastwoodAbstractNewType` — two REAL declarations whose child shape
|
|
1481
|
+
* `[:: ! ERROR constructor]` is byte-identical to the phantom at
|
|
1482
|
+
* `Libraries/Hilde/FamkeConcurrentClean.dcl:8` (`:: !(*World`). No node-INTERNAL rule
|
|
1483
|
+
* separates them; every child, field and name is the same. So the discriminator is
|
|
1484
|
+
* POSITIONAL and uses node positions only: a signature's `::` follows the callable's name
|
|
1485
|
+
* on the same row, a definition's `::` begins its own row. Measured on the same
|
|
1486
|
+
* population, that withdraws 72 and keeps exactly those 2.
|
|
1487
|
+
*
|
|
1488
|
+
* NO TEXT AND NO REGEX. The test is whether the nearest preceding NAMED sibling ENDS on
|
|
1489
|
+
* this node's start row, so no string can be mistaken for a construct and no file-path
|
|
1490
|
+
* convention can stand in for one.
|
|
1491
|
+
*/
|
|
1492
|
+
isSignatureRecoveryTypeDefinition(node) {
|
|
1493
|
+
if (node.childForFieldName("body") || node.childForFieldName("rhs")) return false;
|
|
1494
|
+
let afterColonColon = "";
|
|
1495
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1496
|
+
const c = node.child(i);
|
|
1497
|
+
if (!c || c.type !== "::") continue;
|
|
1498
|
+
const next = node.child(i + 1);
|
|
1499
|
+
if (next) afterColonColon = next.type;
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
if (afterColonColon !== "!" && afterColonColon !== "#" && afterColonColon !== "ERROR") return false;
|
|
1503
|
+
const parent = node.parent;
|
|
1504
|
+
if (!parent) return false;
|
|
1505
|
+
let index = -1;
|
|
1506
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
1507
|
+
const c = parent.child(i);
|
|
1508
|
+
if (c && c.id === node.id) {
|
|
1509
|
+
index = i;
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (index < 0) return false;
|
|
1514
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
1515
|
+
const sibling = parent.child(i);
|
|
1516
|
+
if (!sibling || !sibling.isNamed) continue;
|
|
1517
|
+
return sibling.endPosition.row === node.startPosition.row;
|
|
1518
|
+
}
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
processTypeDefinition(node, ctx, scopeStack) {
|
|
1522
|
+
if (this.isSignatureRecoveryTypeDefinition(node)) return false;
|
|
1523
|
+
const nameNode = node.childForFieldName("name");
|
|
1524
|
+
if (!nameNode?.text) return false;
|
|
1525
|
+
const name = nameNode.text;
|
|
1526
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1527
|
+
const body = node.childForFieldName("body");
|
|
1528
|
+
const rhs = body ? body.childForFieldName("rhs") ?? body : node.childForFieldName("rhs");
|
|
1529
|
+
const form = this.typeForm(rhs);
|
|
1530
|
+
const genericParameters = [];
|
|
1531
|
+
for (const p of this.fields(node, "parameter")) {
|
|
1532
|
+
const inner = p.namedChild(0);
|
|
1533
|
+
if (inner?.text) genericParameters.push(inner.text);
|
|
1534
|
+
}
|
|
1535
|
+
if (body) {
|
|
1536
|
+
for (const e of this.fields(body, "existential")) {
|
|
1537
|
+
const inner = e.namedChild(0);
|
|
1538
|
+
if (inner?.text) genericParameters.push(`!${inner.text}`);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
ctx.symbols.push({
|
|
1542
|
+
name,
|
|
1543
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
1544
|
+
range: this.nodeToRange(node),
|
|
1545
|
+
scope: currentScope?.name,
|
|
1546
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1547
|
+
ownerKind: currentScope?.kind,
|
|
1548
|
+
visibility: "public",
|
|
1549
|
+
genericParameters: genericParameters.length > 0 ? genericParameters : void 0,
|
|
1550
|
+
metadata: { cleanSpecific: true, typeForm: form }
|
|
1551
|
+
});
|
|
1552
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], node);
|
|
1553
|
+
if (rhs && rhs.type !== "data_constructors" && rhs.type !== "record_definition") {
|
|
1554
|
+
this.emitTypeRefs(rhs, name, "type_alias_rhs", ctx);
|
|
1555
|
+
}
|
|
1556
|
+
for (const cNode of this.dataConstructorsOf(rhs)) {
|
|
1557
|
+
const cName = cNode.childForFieldName("name");
|
|
1558
|
+
if (!cName?.text) continue;
|
|
1559
|
+
ctx.symbols.push({
|
|
1560
|
+
name: cName.text,
|
|
1561
|
+
kind: ASTEngine.SymbolKind.Constructor,
|
|
1562
|
+
range: this.nodeToRange(cNode),
|
|
1563
|
+
scope: name,
|
|
1564
|
+
ownerName: name,
|
|
1565
|
+
ownerKind: ASTEngine.SymbolKind.Type,
|
|
1566
|
+
visibility: "public",
|
|
1567
|
+
metadata: { cleanSpecific: true, isADTConstructor: true }
|
|
1568
|
+
});
|
|
1569
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], cNode);
|
|
1570
|
+
for (const arg of this.fields(cNode, "argument")) {
|
|
1571
|
+
this.emitTypeRefs(arg, cName.text, "data_constructor_arg", ctx);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const recordDef = rhs && rhs.type === "record_definition" ? rhs : body ? this.findChild(body, (n) => n.type === "record_definition") : null;
|
|
1575
|
+
if (recordDef) {
|
|
1576
|
+
for (let i = 0; i < recordDef.namedChildCount; i++) {
|
|
1577
|
+
const rf = recordDef.namedChild(i);
|
|
1578
|
+
if (!rf || rf.type !== "record_field") continue;
|
|
1579
|
+
const fieldId = rf.childForFieldName("name");
|
|
1580
|
+
if (!fieldId?.text) continue;
|
|
1581
|
+
ctx.symbols.push({
|
|
1582
|
+
name: fieldId.text,
|
|
1583
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1584
|
+
range: this.nodeToRange(rf),
|
|
1585
|
+
scope: name,
|
|
1586
|
+
ownerName: name,
|
|
1587
|
+
ownerKind: ASTEngine.SymbolKind.Type,
|
|
1588
|
+
visibility: "public",
|
|
1589
|
+
metadata: { cleanSpecific: true, isRecordField: true }
|
|
1590
|
+
});
|
|
1591
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], rf);
|
|
1592
|
+
const ft = rf.childForFieldName("type");
|
|
1593
|
+
if (ft) this.emitTypeRefs(ft, fieldId.text, "record_field_type", ctx);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
scopeStack.push({ node, name, kind: ASTEngine.SymbolKind.Type });
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
/** The `data_constructor` children of a type's rhs, however it is nested. */
|
|
1600
|
+
dataConstructorsOf(rhs) {
|
|
1601
|
+
if (!rhs) return [];
|
|
1602
|
+
if (rhs.type === "data_constructors") return this.filterChildren(rhs, (n) => n.type === "data_constructor");
|
|
1603
|
+
return this.filterChildren(rhs, (n) => n.type === "data_constructor");
|
|
1604
|
+
}
|
|
1605
|
+
/** Which of Clean's six type forms a `rhs` node type denotes. */
|
|
1606
|
+
typeForm(rhs) {
|
|
1607
|
+
if (!rhs) return "abstract";
|
|
1608
|
+
switch (rhs.type) {
|
|
1609
|
+
case "data_constructors":
|
|
1610
|
+
return "adt";
|
|
1611
|
+
case "record_definition":
|
|
1612
|
+
return "record";
|
|
1613
|
+
case "constructor":
|
|
1614
|
+
return "enumerated";
|
|
1615
|
+
default:
|
|
1616
|
+
return "synonym";
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* A class declaration: `class C a | D a where …`.
|
|
1621
|
+
*
|
|
1622
|
+
* Two things the old version did not do. (1) It read the name with
|
|
1623
|
+
* `findChild(node, n => n.type === "class_name")` instead of the `name` field;
|
|
1624
|
+
* the field is exact and the scan is a guess. (2) It published members by
|
|
1625
|
+
* scanning `class_member` children for a `type_signature` and then `continue`d
|
|
1626
|
+
* on every `type_signature` in the walk — so a class member's `special` block
|
|
1627
|
+
* (90 sites) and inline `code` body (74 sites) were never visited, and a class
|
|
1628
|
+
* member that is a `function_declaration` rather than a signature published
|
|
1629
|
+
* nothing. Members now run through the same handlers as everything else, with
|
|
1630
|
+
* the class on the scope stack.
|
|
1631
|
+
*/
|
|
1632
|
+
processClass(node, ctx, scopeStack) {
|
|
1633
|
+
const nameNode = node.childForFieldName("name");
|
|
1634
|
+
if (!nameNode?.text) return false;
|
|
1635
|
+
const name = nameNode.text;
|
|
1636
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1637
|
+
const genericParameters = [];
|
|
1638
|
+
for (const p of this.fields(node, "parameter")) {
|
|
1639
|
+
const inner = p.namedChild(0);
|
|
1640
|
+
if (inner?.text) genericParameters.push(inner.text);
|
|
1641
|
+
}
|
|
1642
|
+
ctx.symbols.push({
|
|
1643
|
+
name,
|
|
1644
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
1645
|
+
range: this.nodeToRange(node),
|
|
1646
|
+
scope: currentScope?.name,
|
|
1647
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1648
|
+
ownerKind: currentScope?.kind,
|
|
1649
|
+
visibility: "public",
|
|
1650
|
+
genericParameters: genericParameters.length > 0 ? genericParameters : void 0,
|
|
1651
|
+
metadata: { cleanSpecific: true, isTypeClass: true }
|
|
1652
|
+
});
|
|
1653
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], node);
|
|
1654
|
+
const context = node.childForFieldName("context");
|
|
1655
|
+
if (context) this.emitContextRefs(context, name, "class_context", ctx);
|
|
1656
|
+
if (node.childForFieldName("type")) ctx.patterns["Algebraic Type Classes"] = true;
|
|
1657
|
+
ctx.patterns["Type Classes"] = true;
|
|
1658
|
+
scopeStack.push({ node, name, kind: ASTEngine.SymbolKind.Interface });
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* An instance declaration: `instance C T where …`.
|
|
1663
|
+
*
|
|
1664
|
+
* THE 253-DEFECT. The old handler required a direct `constructor` child to name
|
|
1665
|
+
* the target type. Measured over the corpus the `argument` FIELD is 782
|
|
1666
|
+
* occurrences on 741 instances, and only 489 are a bare `constructor`; the other
|
|
1667
|
+
* 293 are `array_type` 127 (`instance Array {} a`), `list_type` 97
|
|
1668
|
+
* (`instance toString [a]`), `type_variable` 32, `type_paren` 16,
|
|
1669
|
+
* `question_type` 9, `tuple_type` 8 and `unit_type` 4. On those 253
|
|
1670
|
+
* declarations (51 files) the scan found nothing and the instance was skipped
|
|
1671
|
+
* whole — no Class symbol, no `implements` edge, no member scope, and every
|
|
1672
|
+
* `instance_member` inside it (1,009 members corpus-wide) left ownerless or
|
|
1673
|
+
* attributed to the module. The field is exact.
|
|
1674
|
+
*
|
|
1675
|
+
* Reading the field is not sufficient on its own, though: the argument list is
|
|
1676
|
+
* `constraint* Type`, so the TYPE is the LAST argument that names one, and a
|
|
1677
|
+
* bare uniqueness constraint names none. Taking the first argument named every
|
|
1678
|
+
* constrained instance after its constraint — `instance Array {#} Int` as
|
|
1679
|
+
* `{#} -> Array` — which is most of Clean's own StdEnv. See
|
|
1680
|
+
* `instanceTargetName`.
|
|
1681
|
+
*/
|
|
1682
|
+
processInstance(node, ctx, scopeStack) {
|
|
1683
|
+
const classNode = node.childForFieldName("name");
|
|
1684
|
+
if (!classNode?.text) return false;
|
|
1685
|
+
const className = classNode.text;
|
|
1686
|
+
const args = this.fields(node, "argument");
|
|
1687
|
+
const typeArgs = args.filter((a) => this.instanceTargetName(a) !== null);
|
|
1688
|
+
const targetNames = [];
|
|
1689
|
+
for (const a of args) {
|
|
1690
|
+
const t = this.instanceTargetName(a);
|
|
1691
|
+
if (t) targetNames.push(t);
|
|
1692
|
+
}
|
|
1693
|
+
const targetType = typeArgs.length > 0 ? this.instanceTargetName(typeArgs[typeArgs.length - 1]) : null;
|
|
1694
|
+
if (!targetType) return false;
|
|
1695
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1696
|
+
const targetIsPolymorphic = typeArgs[typeArgs.length - 1]?.type === "type_variable";
|
|
1697
|
+
if (!targetIsPolymorphic) {
|
|
1698
|
+
ctx.symbols.push({
|
|
1699
|
+
// THE IMPLEMENTATION IS NAMESPACED UNDER ITS TARGET. It used to be named EXACTLY the
|
|
1700
|
+
// target type, and on a per-file analyzer that is a PHANTOM DECLARATION: a file whose
|
|
1701
|
+
// only statement about `Int` is `instance Show Int` published a `class` symbol named
|
|
1702
|
+
// `Int`, which counts as a declaration of `Int` in the project's name space and therefore
|
|
1703
|
+
// turns every `references -> Int` edge elsewhere into an ambiguity. Measured on the
|
|
1704
|
+
// 312-file population (`validation/m14/fixtures/clean/phantom-census.mts`): **699 such
|
|
1705
|
+
// containers in 94 files, 93 of them named after a type the SAME file also declares for
|
|
1706
|
+
// real, 150 of them not an identifier at all** (`.a`, `[a]`, `?` — instance heads that
|
|
1707
|
+
// are type EXPRESSIONS, so the name could never match any reference in the first place).
|
|
1708
|
+
// Deleting them alone reaches **1,009 of the lane's 6,856 residual edges (14.7%)** —
|
|
1709
|
+
// 574 `references` whose target has no real declaration in the project at all, 366 whose
|
|
1710
|
+
// target has exactly one real declaration beside the phantoms (those RESOLVE once the
|
|
1711
|
+
// phantom stops counting), and 69 `exports` that were never exports.
|
|
1712
|
+
//
|
|
1713
|
+
// The naming is the RUST lane's (`impl Foo` publishes `Foo::impl`, so the implementation
|
|
1714
|
+
// is addressable without claiming Foo is declared here), and the reason is the same: an
|
|
1715
|
+
// instance is a DECLARATION OF AN IMPLEMENTATION, not of its target type. What made this
|
|
1716
|
+
// lane worse than Rust's is that Clean's instance heads are usually types from StdEnv
|
|
1717
|
+
// (`Int`, `Char`, `Real`, `Bool` — the five most common container names), so the phantom
|
|
1718
|
+
// landed on exactly the names the language uses most.
|
|
1719
|
+
//
|
|
1720
|
+
// THE MEMBERS KEEP THE NOMINAL OWNER (`frameOwner` below), which is what keeps this fix
|
|
1721
|
+
// surgical: `contains` still reads `Int -> show`, `overrides` still reads `Int.show ->
|
|
1722
|
+
// Show.show`, and the member's `qualifiedName` is unchanged — every observable edge
|
|
1723
|
+
// surface is identical to before, and the ONLY difference is that the file no longer
|
|
1724
|
+
// declares `Int`. Moving the owner as well would rename 2,000+ edges to fix a declaration
|
|
1725
|
+
// count, which is the wrong trade: the endpoint of a `contains` edge is the name a reader
|
|
1726
|
+
// navigates to, and `Int.show` is what Clean itself calls that member.
|
|
1727
|
+
name: `${targetType}${INSTANCE_CONTAINER_SUFFIX}`,
|
|
1728
|
+
// `impl`, not `class`: `class` is an EXPORTABLE kind, so the container became a module
|
|
1729
|
+
// export — `Prelude/Data/List.dcl` exported `[]` and `.a` because it holds instances whose
|
|
1730
|
+
// heads are those type expressions (69 measured `exports` rows, all of them naming a
|
|
1731
|
+
// phantom). An instance is not part of a module's interface.
|
|
1732
|
+
kind: ASTEngine.SymbolKind.Impl,
|
|
1733
|
+
range: this.nodeToRange(node),
|
|
1734
|
+
scope: currentScope?.name,
|
|
1735
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1736
|
+
ownerKind: currentScope?.kind,
|
|
1737
|
+
visibility: "public",
|
|
1738
|
+
metadata: {
|
|
1739
|
+
cleanSpecific: true,
|
|
1740
|
+
className,
|
|
1741
|
+
targetType,
|
|
1742
|
+
targetTypes: targetNames,
|
|
1743
|
+
// The instance's target as the SOURCE spells it (`List a`), kept beside the nameable
|
|
1744
|
+
// identity (`List`) so a reader can see which instance this is when one type has
|
|
1745
|
+
// several — the identity is what resolves, the spelling is what distinguishes.
|
|
1746
|
+
targetSpelling: typeArgs[typeArgs.length - 1]?.text,
|
|
1747
|
+
isTypeClassInstance: true,
|
|
1748
|
+
// The container's name is DERIVED (`<target>::instance`), so the target it was derived
|
|
1749
|
+
// from travels with it: a consumer reading `Int::instance` should not have to strip a
|
|
1750
|
+
// suffix to learn which type the instance is for, and the pair must not drift apart.
|
|
1751
|
+
instanceTarget: targetType
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], node);
|
|
1755
|
+
}
|
|
1756
|
+
this.pushRel(
|
|
1757
|
+
ctx,
|
|
1758
|
+
targetType,
|
|
1759
|
+
className,
|
|
1760
|
+
ASTEngine.RelationshipType.Implements,
|
|
1761
|
+
1,
|
|
1762
|
+
{
|
|
1763
|
+
cleanSpecific: true,
|
|
1764
|
+
isInstance: true,
|
|
1765
|
+
className,
|
|
1766
|
+
// Almost every class is imported (`instance Eq Int` names StdEnv's `Eq`),
|
|
1767
|
+
// so the flag records the normal case explicitly instead of leaving the
|
|
1768
|
+
// reader to infer it from a missing symbol.
|
|
1769
|
+
//
|
|
1770
|
+
// The second spelling is the operator case, and it is not cosmetic: a class
|
|
1771
|
+
// DECLARATION spells an operator-named class parenthesised (`class (==) infix 4 a ::`,
|
|
1772
|
+
// clean-stdlib/StdOverloaded.icl:23) while the INSTANCE spells it bare
|
|
1773
|
+
// (`instance == Bool`, clean-stdlib/StdBool.dcl:10). `declaredTypeNames` therefore
|
|
1774
|
+
// holds `(==)` and the lookup below held only `==`, so 24 `implements` edges on
|
|
1775
|
+
// the 312-file corpus carried `isExternalClass: true` for a class declared in the
|
|
1776
|
+
// SAME file — a false statement in the one field whose whole job is to say which
|
|
1777
|
+
// of the two it is. `classMembers` gets the same treatment where `overrides` reads it.
|
|
1778
|
+
isExternalClass: !ctx.declaredTypeNames.has(className) && !this.isLocallyDeclaredOperatorClass(ctx, className),
|
|
1779
|
+
// The instance's target is a type VARIABLE, so the instance is polymorphic:
|
|
1780
|
+
// it publishes no symbol (see above) and this flag says why the edge has no
|
|
1781
|
+
// symbol at its source.
|
|
1782
|
+
isPolymorphicInstance: targetIsPolymorphic || void 0
|
|
1783
|
+
},
|
|
1784
|
+
this.lineOf(classNode)
|
|
1785
|
+
);
|
|
1786
|
+
for (let i = 1; i < args.length; i++) this.emitTypeRefs(args[i], targetType, "instance_target", ctx);
|
|
1787
|
+
const context = node.childForFieldName("context");
|
|
1788
|
+
if (context) this.emitContextRefs(context, targetType, "instance_context", ctx);
|
|
1789
|
+
if (node.childForFieldName("type")) ctx.patterns["Qualified Instances"] = true;
|
|
1790
|
+
if (node.childForFieldName("body")) ctx.patterns["Inline Machine Code"] = true;
|
|
1791
|
+
ctx.patterns["Type Classes"] = true;
|
|
1792
|
+
const frameOwner = !targetIsPolymorphic ? targetType : ctx.declaredClassNames.has(className) ? className : ctx.currentModule ?? className;
|
|
1793
|
+
const classMem = ctx.classMembers.get(className);
|
|
1794
|
+
if (classMem && classMem.size > 0) {
|
|
1795
|
+
if (frameOwner !== className) {
|
|
1796
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1797
|
+
const m = node.namedChild(i);
|
|
1798
|
+
if (m?.type !== "instance_member") continue;
|
|
1799
|
+
const memberName = this.memberDeclaredName(m);
|
|
1800
|
+
if (!memberName || !classMem.has(memberName)) continue;
|
|
1801
|
+
const nameNode = m.namedChild(0)?.childForFieldName("name") ?? m.namedChild(0);
|
|
1802
|
+
this.pushRel(
|
|
1803
|
+
ctx,
|
|
1804
|
+
`${frameOwner}.${memberName}`,
|
|
1805
|
+
`${className}.${memberName}`,
|
|
1806
|
+
ASTEngine.RelationshipType.Overrides,
|
|
1807
|
+
1,
|
|
1808
|
+
{ cleanSpecific: true, isTypeClassOverride: true, className, instanceType: frameOwner, member: memberName },
|
|
1809
|
+
this.lineOf(nameNode)
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
scopeStack.push({
|
|
1815
|
+
node,
|
|
1816
|
+
name: frameOwner,
|
|
1817
|
+
kind: ASTEngine.SymbolKind.Class
|
|
1818
|
+
});
|
|
1819
|
+
return true;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* The TYPE an `argument` names, or null when the argument is a bare UNIQUENESS
|
|
1823
|
+
* CONSTRAINT rather than a type.
|
|
1824
|
+
*
|
|
1825
|
+
* Clean's instance head is `instance Class constraint* Type`, and the grammar puts
|
|
1826
|
+
* a constraint that applies to the WHOLE instance in the same `argument` field as
|
|
1827
|
+
* the type, so the argument list is not all types:
|
|
1828
|
+
*
|
|
1829
|
+
* instance Array {!} a args = [ array_type({!}), type_variable(a) ]
|
|
1830
|
+
* instance Array {#} Int args = [ array_type({#}), constructor(Int) ]
|
|
1831
|
+
* instance Array {#} {32#.a} args = [ array_type({#}), array_type({32#.a}) ]
|
|
1832
|
+
* instance == {#Char} args = [ array_type({#Char}) ] <- ONE arg, and
|
|
1833
|
+
* it IS the type
|
|
1834
|
+
*
|
|
1835
|
+
* Taking the FIRST argument named every constrained instance after its
|
|
1836
|
+
* CONSTRAINT: `instance Array {#} Int` became `{#} -> Array`, and
|
|
1837
|
+
* `instance Array {32#} Int` became `32 -> Array` (the inner child of `{32#}`).
|
|
1838
|
+
* That is the misnamed `implements` edge and instance symbol behind every
|
|
1839
|
+
* constrained instance — the shape Clean's own StdEnv uses most, since `Array`
|
|
1840
|
+
* alone has six. Taking the LAST argument fixes the head but still returns `32`
|
|
1841
|
+
* for `{32#.a}`, because an `array_type` may hold a constraint LEVEL and a TYPE
|
|
1842
|
+
* together.
|
|
1843
|
+
*
|
|
1844
|
+
* So the walk is STRUCTURAL: inside a `type_paren` / `array_type` /
|
|
1845
|
+
* `uniqueness_type`, take the first named child that is not a constraint marker.
|
|
1846
|
+
* `{!}` and `{#}` have no such child and are therefore constraints (null);
|
|
1847
|
+
* `{#Char}` and `{32#.a}` have a `dotted_type` and are therefore types. The
|
|
1848
|
+
* earlier version of this comment claimed the same thing via a text test on
|
|
1849
|
+
* `{#…}`; the node shape says it outright, so there is no text inspection left.
|
|
1850
|
+
*/
|
|
1851
|
+
instanceTargetName(arg) {
|
|
1852
|
+
switch (arg.type) {
|
|
1853
|
+
case "constructor":
|
|
1854
|
+
case "type_function":
|
|
1855
|
+
case "dotted_type":
|
|
1856
|
+
return arg.text;
|
|
1857
|
+
// A LIST OR TUPLE TYPE'S NAME HAS NO SPACES IN IT. `instance length [ !]`
|
|
1858
|
+
// (clean-stdlib/StdOverloadedList.icl:142) is the strict list type constructor, and the
|
|
1859
|
+
// source's own spacing inside the brackets is layout, not spelling: the type `[ !]` and the
|
|
1860
|
+
// type `[!]` are the same type and Clean's own declaration writes `[!]`. Leaving the space in
|
|
1861
|
+
// gave the instance's members a `contains` source and an `implements` target that no symbol
|
|
1862
|
+
// can match. Measured: 3 edges on the 312-file population, and 0 after the normalization.
|
|
1863
|
+
case "tuple_type":
|
|
1864
|
+
case "list_type":
|
|
1865
|
+
return stripWhitespace(arg.text) || arg.text;
|
|
1866
|
+
case "type_application": {
|
|
1867
|
+
const head = arg.namedChild(0);
|
|
1868
|
+
if (!head) return arg.text;
|
|
1869
|
+
const headName = this.instanceTargetName(head);
|
|
1870
|
+
if (!headName) return arg.text;
|
|
1871
|
+
return this.unparenthesize(headName) ?? headName;
|
|
1872
|
+
}
|
|
1873
|
+
case "question_type": {
|
|
1874
|
+
const head = arg.namedChild(0);
|
|
1875
|
+
return head?.text || arg.text;
|
|
1876
|
+
}
|
|
1877
|
+
case "type_variable":
|
|
1878
|
+
return arg.namedChild(0)?.text ?? arg.text;
|
|
1879
|
+
case "type_paren":
|
|
1880
|
+
case "array_type":
|
|
1881
|
+
case "uniqueness_type":
|
|
1882
|
+
case "unique_var_type": {
|
|
1883
|
+
for (let i = 0; i < arg.namedChildCount; i++) {
|
|
1884
|
+
const inner = arg.namedChild(i);
|
|
1885
|
+
if (!inner || CONSTRAINT_MARKER_TYPES.has(inner.type)) continue;
|
|
1886
|
+
const name = this.instanceTargetName(inner);
|
|
1887
|
+
if (name) return name;
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
default:
|
|
1892
|
+
return arg.text;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* A Clean generic: `generic bimap a b | bimap b a :: .a -> .b`.
|
|
1897
|
+
*
|
|
1898
|
+
* A generic is a class over TYPES rather than values — Clean's type-level
|
|
1899
|
+
* dispatch, the machinery behind the "HACK: dictionaries for all generics"
|
|
1900
|
+
* comment in `StdGeneric.dcl`. It is therefore an `Interface` with generic
|
|
1901
|
+
* parameters, and the `| bimap b a` context is a requirement on the generic
|
|
1902
|
+
* itself, emitted as a `references` edge like any other context. 11
|
|
1903
|
+
* declarations corpus-wide.
|
|
1904
|
+
*/
|
|
1905
|
+
processGenericDeclaration(node, ctx, currentScope) {
|
|
1906
|
+
const nameNode = node.childForFieldName("name");
|
|
1907
|
+
if (!nameNode?.text) return;
|
|
1908
|
+
const name = nameNode.text;
|
|
1909
|
+
const genericParameters = [];
|
|
1910
|
+
for (const p of this.fields(node, "parameter")) {
|
|
1911
|
+
const inner = p.namedChild(0);
|
|
1912
|
+
if (inner?.text) genericParameters.push(inner.text);
|
|
1913
|
+
}
|
|
1914
|
+
const contextArgs = [];
|
|
1915
|
+
for (const p of this.fields(node, "context_arg")) {
|
|
1916
|
+
const inner = p.namedChild(0);
|
|
1917
|
+
if (inner?.text) contextArgs.push(inner.text);
|
|
1918
|
+
}
|
|
1919
|
+
const contextName = node.childForFieldName("context")?.text;
|
|
1920
|
+
ctx.symbols.push({
|
|
1921
|
+
name,
|
|
1922
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
1923
|
+
range: this.nodeToRange(node),
|
|
1924
|
+
scope: currentScope?.name,
|
|
1925
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1926
|
+
ownerKind: currentScope?.kind,
|
|
1927
|
+
visibility: "public",
|
|
1928
|
+
genericParameters: genericParameters.length > 0 ? genericParameters : void 0,
|
|
1929
|
+
metadata: {
|
|
1930
|
+
cleanSpecific: true,
|
|
1931
|
+
isGeneric: true,
|
|
1932
|
+
genericContext: contextName,
|
|
1933
|
+
genericContextArgs: contextArgs,
|
|
1934
|
+
genericType: this.unnamedChildOfType(node, "type_function")?.text
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], node);
|
|
1938
|
+
if (contextName && contextName !== name) {
|
|
1939
|
+
this.pushRel(
|
|
1940
|
+
ctx,
|
|
1941
|
+
name,
|
|
1942
|
+
contextName,
|
|
1943
|
+
ASTEngine.RelationshipType.References,
|
|
1944
|
+
0.95,
|
|
1945
|
+
{ cleanSpecific: true, relationshipType: "generic_context" },
|
|
1946
|
+
this.lineOf(node.childForFieldName("context"))
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
ctx.patterns["Generics"] = true;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* `derive bimap c` — the generic `bimap` is derived for the type `c`.
|
|
1953
|
+
*
|
|
1954
|
+
* The same shape as an instance: an implementation of a class at a type, so the
|
|
1955
|
+
* edge is `implements` from the derived type to the generic, with the generic's
|
|
1956
|
+
* own node as the name-bearing site line. 43 sites corpus-wide (all in
|
|
1957
|
+
* `StdGeneric.dcl`), and every one published nothing before: the
|
|
1958
|
+
* `derive_declaration` node type was named nowhere in the analyzer.
|
|
1959
|
+
*/
|
|
1960
|
+
processDeriveDeclaration(node, ctx) {
|
|
1961
|
+
const nameNode = node.childForFieldName("name");
|
|
1962
|
+
if (!nameNode?.text) return;
|
|
1963
|
+
const genericName = nameNode.text;
|
|
1964
|
+
let target = null;
|
|
1965
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1966
|
+
const c = node.namedChild(i);
|
|
1967
|
+
if (!c || c.startIndex === nameNode.startIndex) continue;
|
|
1968
|
+
if (!DERIVE_TARGET_NODE_TYPES.has(c.type)) continue;
|
|
1969
|
+
const spelled = c.type === "type_variable" ? c.namedChild(0)?.text ?? c.text : c.text;
|
|
1970
|
+
if (!spelled || isReservedDeclarationName(spelled)) continue;
|
|
1971
|
+
target = c;
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
if (!target) return;
|
|
1975
|
+
const targetName = target.type === "type_variable" ? target.namedChild(0)?.text ?? target.text : target.text;
|
|
1976
|
+
if (!targetName) return;
|
|
1977
|
+
this.pushRel(
|
|
1978
|
+
ctx,
|
|
1979
|
+
targetName,
|
|
1980
|
+
genericName,
|
|
1981
|
+
ASTEngine.RelationshipType.Implements,
|
|
1982
|
+
1,
|
|
1983
|
+
{ cleanSpecific: true, isDerive: true, genericName },
|
|
1984
|
+
this.lineOf(nameNode)
|
|
1985
|
+
);
|
|
1986
|
+
ctx.patterns["Generics"] = true;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* A callable definition: `function_declaration`, `operator_definition` or
|
|
1990
|
+
* `generic_case_definition`.
|
|
1991
|
+
*
|
|
1992
|
+
* The old code handled the first two and nothing else.
|
|
1993
|
+
* `generic_case_definition` (52 sites) is the third shape Clean uses to define a
|
|
1994
|
+
* callable — `bimap{|PAIR|} fx _ fy _ (PAIR x y) = …` — and the six `bimap{|…|}`
|
|
1995
|
+
* case equations are ONE function, so they merge into one symbol with the case
|
|
1996
|
+
* kinds recorded, exactly as multi-clause definitions merge.
|
|
1997
|
+
*/
|
|
1998
|
+
/**
|
|
1999
|
+
* The owner of a `where`/`with`/`let` binder.
|
|
2000
|
+
*
|
|
2001
|
+
* Normally the enclosing FRAME, which is what makes a local's identity scoped and
|
|
2002
|
+
* its `contains` edge resolvable. But a local can be reached with no frame at all
|
|
2003
|
+
* when the pinned grammar could not read the function it belongs to: a
|
|
2004
|
+
* tuple-destructuring `where` binding — `(transition_nr, transition) =
|
|
2005
|
+
* SelectTransition 0 head …` — is not in the grammar, so it becomes an ERROR node
|
|
2006
|
+
* and everything after it inside that function sits inside the ERROR subtree,
|
|
2007
|
+
* detached from the `function_declaration` that would have opened the frame.
|
|
2008
|
+
*
|
|
2009
|
+
* Measured on the 2,179-file corpus: 7 such locals — `left` and `right` in
|
|
2010
|
+
* `ObjectIO/…/turing/tm.icl`, `key` and `keyIndex` in `Ed/EdKeyMapping.icl`,
|
|
2011
|
+
* `toolBoxFun` and `okWindowKeyboardAtt` in `ObjectIO/…/keyboardevent.icl`, and
|
|
2012
|
+
* `sp` in `Charlie.icl` — each found by the honesty gate as a symbol carrying an
|
|
2013
|
+
* `ownerKind` with no `ownerName`.
|
|
2014
|
+
*
|
|
2015
|
+
* Falling back to the MODULE is the honest answer rather than a fudge: the binder
|
|
2016
|
+
* really is declared in this file, and the module is the nearest owner that
|
|
2017
|
+
* EXISTS. A symbol with no owner resolves to nothing, and the file-level count
|
|
2018
|
+
* records that the enclosing function was lost to damage instead of pretending
|
|
2019
|
+
* the local is module-level by design.
|
|
2020
|
+
*/
|
|
2021
|
+
localOwner(ctx) {
|
|
2022
|
+
if (ctx.currentFrame !== void 0) return ctx.currentFrame;
|
|
2023
|
+
if (ctx.currentModule !== void 0) ctx.detachedLocals++;
|
|
2024
|
+
return ctx.currentModule;
|
|
2025
|
+
}
|
|
2026
|
+
processCallableNode(node, ctx, currentScope, inErrorRegion = false) {
|
|
2027
|
+
const declared = this.rawCallableDeclaredName(node);
|
|
2028
|
+
if (!declared) return null;
|
|
2029
|
+
if (isReservedDeclarationName(declared)) {
|
|
2030
|
+
this.publishParseDamageSymbol(node, declared, ctx, currentScope);
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
const name = declared;
|
|
2034
|
+
const isOperator = node.type === "operator_definition" || node.type === "generic_case_definition";
|
|
2035
|
+
const isLocal = this.isLocalScope(currentScope);
|
|
2036
|
+
const kind = this.callableKind(currentScope, isLocal);
|
|
2037
|
+
const key = declarationKey(ctx.currentFrame, this.ownerIdentity(currentScope, ctx), name);
|
|
2038
|
+
const existingIdx = this.callableMergeIndex(key, ctx);
|
|
2039
|
+
if (existingIdx !== void 0) {
|
|
2040
|
+
const existing = ctx.symbols[existingIdx];
|
|
2041
|
+
const fromDeclaration = existing.metadata?.isDeclaration === true;
|
|
2042
|
+
if (fromDeclaration || !existing.range) {
|
|
2043
|
+
existing.range = this.nodeToRange(node);
|
|
2044
|
+
} else {
|
|
2045
|
+
const r = this.nodeToRange(node);
|
|
2046
|
+
const prevEnd = existing.range.endLine ?? r.startLine;
|
|
2047
|
+
const prevEndIndex = existing.range.endIndex ?? node.startIndex;
|
|
2048
|
+
existing.range = {
|
|
2049
|
+
...existing.range,
|
|
2050
|
+
endLine: Math.max(prevEnd, r.endLine),
|
|
2051
|
+
endColumn: r.endLine > prevEnd ? r.endColumn : existing.range.endColumn ?? r.endColumn,
|
|
2052
|
+
endIndex: Math.max(prevEndIndex, r.endIndex ?? prevEndIndex)
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
delete existing.metadata?.isDeclaration;
|
|
2056
|
+
if (existing.isAbstract) existing.isAbstract = false;
|
|
2057
|
+
this.mergeComments(existing, node);
|
|
2058
|
+
const md2 = existing.metadata ?? (existing.metadata = {});
|
|
2059
|
+
if (isOperator && !md2.isOperatorDefinition) md2.isOperatorDefinition = true;
|
|
2060
|
+
if (node.type === "generic_case_definition") this.addCaseKind(md2, node);
|
|
2061
|
+
this.joinParameters(existing, node, ctx, this.ownerIdentity(currentScope, ctx));
|
|
2062
|
+
return { name, kind, isLocal, key };
|
|
2063
|
+
}
|
|
2064
|
+
const md = { cleanSpecific: true };
|
|
2065
|
+
if (isOperator) md.isOperatorDefinition = true;
|
|
2066
|
+
const scopedName = isLocal ? ctx.currentFrame === void 0 ? name : `${ctx.currentFrame}.${name}` : void 0;
|
|
2067
|
+
if (scopedName) {
|
|
2068
|
+
md.isLocalDefinition = true;
|
|
2069
|
+
md.scopedName = scopedName;
|
|
2070
|
+
}
|
|
2071
|
+
if (node.type === "generic_case_definition") this.addCaseKind(md, node);
|
|
2072
|
+
const sym = {
|
|
2073
|
+
name,
|
|
2074
|
+
kind,
|
|
2075
|
+
range: this.nodeToRange(node),
|
|
2076
|
+
scope: currentScope?.name,
|
|
2077
|
+
// The store's `qualified_name`, which is what relationship endpoints are
|
|
2078
|
+
// resolved against. A module-level symbol needs none: its name is already
|
|
2079
|
+
// unique in the file, and the store falls back to `name`.
|
|
2080
|
+
...scopedName ? { qualifiedName: scopedName } : {},
|
|
2081
|
+
ownerName: isLocal ? this.localOwner(ctx) : currentScope?.name || ctx.currentModule,
|
|
2082
|
+
ownerKind: isLocal ? ASTEngine.SymbolKind.Function : currentScope?.kind,
|
|
2083
|
+
// A callable reached only through an ERROR region has no knowable scope, so it
|
|
2084
|
+
// is not part of the module's API and must not claim to be exported.
|
|
2085
|
+
visibility: isLocal || inErrorRegion ? "local" : "public",
|
|
2086
|
+
complexity: 1,
|
|
2087
|
+
metadata: md
|
|
2088
|
+
};
|
|
2089
|
+
ctx.symbols.push(sym);
|
|
2090
|
+
ctx.functionSymbolIndex[key] = ctx.symbols.length - 1;
|
|
2091
|
+
this.joinParameters(sym, node, ctx, this.ownerIdentity(currentScope, ctx));
|
|
2092
|
+
return { name, kind, isLocal, key };
|
|
2093
|
+
}
|
|
2094
|
+
addCaseKind(md, node) {
|
|
2095
|
+
const kind = node.childForFieldName("kind")?.text;
|
|
2096
|
+
if (!kind) return;
|
|
2097
|
+
const kinds = md.caseKinds ?? (md.caseKinds = []);
|
|
2098
|
+
if (!kinds.includes(kind)) kinds.push(kind);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* A module-level macro: `NSWindowCloseButton :== 0`, `(FWI) f i :== fwritei i f`.
|
|
2102
|
+
*
|
|
2103
|
+
* 583 sites corpus-wide with the analyzer naming `macro_definition` nowhere, so
|
|
2104
|
+
* every one published nothing AND every application inside a macro body
|
|
2105
|
+
* attributed to whatever frame happened to be open. A macro is a
|
|
2106
|
+
* `SymbolKind.Macro` (the kind exists for exactly this: Rust `macro_rules!`) and
|
|
2107
|
+
* it is a frame, because a macro body is expanded at its use site and the calls
|
|
2108
|
+
* in it are the module's calls.
|
|
2109
|
+
*/
|
|
2110
|
+
processMacro(node, ctx, currentScope) {
|
|
2111
|
+
const name = this.normalizeParenthesized(node.childForFieldName("name"));
|
|
2112
|
+
if (!name) return null;
|
|
2113
|
+
if (isReservedDeclarationName(name)) return null;
|
|
2114
|
+
const isLocal = this.isLocalScope(currentScope);
|
|
2115
|
+
const key = declarationKey(ctx.currentFrame, this.ownerIdentity(currentScope, ctx), name);
|
|
2116
|
+
if (ctx.functionSymbolIndex[key] === void 0) {
|
|
2117
|
+
const macroSym = {
|
|
2118
|
+
name,
|
|
2119
|
+
kind: ASTEngine.SymbolKind.Macro,
|
|
2120
|
+
range: this.nodeToRange(node),
|
|
2121
|
+
scope: currentScope?.name,
|
|
2122
|
+
ownerName: isLocal ? this.localOwner(ctx) : currentScope?.name || ctx.currentModule,
|
|
2123
|
+
ownerKind: isLocal ? ASTEngine.SymbolKind.Function : currentScope?.kind,
|
|
2124
|
+
visibility: isLocal ? "local" : "public",
|
|
2125
|
+
qualifiedName: isLocal ? ctx.currentFrame === void 0 ? name : `${ctx.currentFrame}.${name}` : void 0,
|
|
2126
|
+
metadata: {
|
|
2127
|
+
cleanSpecific: true,
|
|
2128
|
+
isMacroDefinition: true,
|
|
2129
|
+
isLocalDefinition: isLocal || void 0,
|
|
2130
|
+
scopedName: isLocal ? ctx.currentFrame === void 0 ? name : `${ctx.currentFrame}.${name}` : void 0
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
this.attachComments(macroSym, node);
|
|
2134
|
+
ctx.symbols.push(macroSym);
|
|
2135
|
+
ctx.functionSymbolIndex[key] = ctx.symbols.length - 1;
|
|
2136
|
+
}
|
|
2137
|
+
ctx.patterns["Macros"] = true;
|
|
2138
|
+
return { name, kind: ASTEngine.SymbolKind.Macro, isLocal, key };
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* `foreign export f` — a C function re-exported under Clean's calling
|
|
2142
|
+
* convention. 41 sites corpus-wide, published nothing before. It is a real
|
|
2143
|
+
* callable and an explicit export, so it is a `Function` symbol flagged
|
|
2144
|
+
* `isForeignExport` and it enters the module's export surface.
|
|
2145
|
+
*/
|
|
2146
|
+
processForeignExport(node, ctx, currentScope) {
|
|
2147
|
+
const nameNode = node.childForFieldName("name");
|
|
2148
|
+
if (!nameNode?.text) return;
|
|
2149
|
+
const name = nameNode.text;
|
|
2150
|
+
ctx.patterns["Foreign Function Interface"] = true;
|
|
2151
|
+
const line = this.lineOf(node);
|
|
2152
|
+
const key = declarationKey(ctx.currentFrame, currentScope?.name, name);
|
|
2153
|
+
const existingIdx = ctx.functionSymbolIndex[key];
|
|
2154
|
+
if (existingIdx !== void 0) {
|
|
2155
|
+
const existing = ctx.symbols[existingIdx];
|
|
2156
|
+
const md = existing.metadata ?? (existing.metadata = {});
|
|
2157
|
+
md.isForeignExport = true;
|
|
2158
|
+
if (typeof md.foreignExportLine !== "number") md.foreignExportLine = line;
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
ctx.symbols.push({
|
|
2162
|
+
name,
|
|
2163
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
2164
|
+
range: this.nodeToRange(node),
|
|
2165
|
+
scope: currentScope?.name,
|
|
2166
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
2167
|
+
ownerKind: currentScope?.kind,
|
|
2168
|
+
visibility: "public",
|
|
2169
|
+
metadata: { cleanSpecific: true, isForeignExport: true, foreignExportLine: line, isDeclaration: true }
|
|
2170
|
+
});
|
|
2171
|
+
this.attachComments(ctx.symbols[ctx.symbols.length - 1], node);
|
|
2172
|
+
ctx.functionSymbolIndex[key] = ctx.symbols.length - 1;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* `($) infixr 9` — an operator fixity declaration. 17 sites corpus-wide,
|
|
2176
|
+
* published nothing before. A fixity names an OPERATOR that is declared
|
|
2177
|
+
* elsewhere (usually a macro or an `operator_definition`), so it emits no symbol
|
|
2178
|
+
* of its own: the fixity is attached to that operator's symbol by
|
|
2179
|
+
* `attachFixities`, and the record is kept on the file so a fixity on an
|
|
2180
|
+
* operator this file does not define stays visible instead of being dropped.
|
|
2181
|
+
*/
|
|
2182
|
+
processFixityDeclaration(node, ctx) {
|
|
2183
|
+
const opNode = node.childForFieldName("operator");
|
|
2184
|
+
if (!opNode) return;
|
|
2185
|
+
const name = this.normalizeParenthesized(opNode);
|
|
2186
|
+
if (!name) return;
|
|
2187
|
+
ctx.patterns["Operator Fixity Declarations"] = true;
|
|
2188
|
+
ctx.fixities.push({
|
|
2189
|
+
name,
|
|
2190
|
+
associativity: node.childForFieldName("associativity")?.text,
|
|
2191
|
+
precedence: node.childForFieldName("precedence")?.text,
|
|
2192
|
+
line: this.lineOf(node)
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* THE MODULE-QUALIFIED IDENTITY OF EVERY MODULE-LEVEL SYMBOL — `StdPathname.Pathname`, `PmTypes.Project`.
|
|
2197
|
+
*
|
|
2198
|
+
* A Clean module is DECLARED in `.dcl` and IMPLEMENTED in `.icl`, so a type, function, class or macro at
|
|
2199
|
+
* module scope is ONE declaration written in TWO files. The incoming gate models that with
|
|
2200
|
+
* `--binding-collapse`: a key becomes ONE declaration iff every declaration answering to it carries the
|
|
2201
|
+
* same NON-EMPTY `qualifiedName` — and the gate requires that name to DIFFER from the bare name, because
|
|
2202
|
+
* `pool-worker.ts` falls back to `qualifiedName = name` for a file-scope symbol and a fallback is not a
|
|
2203
|
+
* qualification. This analyzer published NO qualifiedName for module-level symbols ("its name is already
|
|
2204
|
+
* unique in the file"), so every module-level `.dcl`/`.icl` pair reached the gate as TWO declarations and
|
|
2205
|
+
* the pair was counted as a name collision.
|
|
2206
|
+
*
|
|
2207
|
+
* MEASURED on the 312-file arm (2026-10-28, whole population): 1,808 `references` ambiguous, and 797 of
|
|
2208
|
+
* them name a module-level pair like this — `Pathname` 143 (`StdPathname.dcl` + `.icl`), `Modulename` 46,
|
|
2209
|
+
* `Project` 34, `StaticLibInfo` 32, `CompilerOptions` 30, `UtilStrictLists` 27 — plus 28 of the 40
|
|
2210
|
+
* ambiguous `exports` rows (operator `&&`, `:!`, `!!|` written in both halves). The lane already ruled the
|
|
2211
|
+
* pair ONE declaration for class/instance MEMBERS, where the store's `Owner.member` spelling made the
|
|
2212
|
+
* collapse fire (`overrides` 2/306 -> 306/306, 719 keys collapsed); the same ruling was simply not
|
|
2213
|
+
* reachable for module scope, because there was no qualified spelling for it to fire on.
|
|
2214
|
+
*
|
|
2215
|
+
* WHAT IS NOT TOUCHED, and each exclusion is a fact about the symbol rather than a convenience:
|
|
2216
|
+
*
|
|
2217
|
+
* - a MODULE symbol (its name IS the module; there is nothing above it to qualify with), and it is the
|
|
2218
|
+
* module's own name, so `qualifiedName = name` would be the fallback the gate rightly ignores;
|
|
2219
|
+
* - a CLASS/INSTANCE MEMBER (`ownerName` is the class or the instance's target, not the module) — those
|
|
2220
|
+
* already carry `Owner.member`, which is what `overrides` and `implements` endpoints are spelled
|
|
2221
|
+
* against;
|
|
2222
|
+
* - an ADT CONSTRUCTOR or RECORD FIELD (`ownerName` is the type that declares it);
|
|
2223
|
+
* - a LOCAL (`visibility: "local"; it keeps its frame-scoped spelling and is never part of the module
|
|
2224
|
+
* interface).
|
|
2225
|
+
*
|
|
2226
|
+
* THE IDENTITY IS NOT INVENTED. Clean names a module-scope entity `Module.name` — that is the spelling a
|
|
2227
|
+
* consumer writes when two modules declare the same name, and it is what the module's own `module` header
|
|
2228
|
+
* plus the declaration produce. The bare `name` stays exactly as the source writes it, so every existing
|
|
2229
|
+
* endpoint, export and fixture pin is unchanged; this field is the QUALIFIED spelling beside it, which is
|
|
2230
|
+
* what the store resolves relationship endpoints against.
|
|
2231
|
+
*/
|
|
2232
|
+
qualifyModuleLevelSymbols(ctx) {
|
|
2233
|
+
const module = ctx.currentModule;
|
|
2234
|
+
if (!module) return;
|
|
2235
|
+
for (const s of ctx.symbols) {
|
|
2236
|
+
if (s.qualifiedName) continue;
|
|
2237
|
+
if (s.kind === ASTEngine.SymbolKind.Module) continue;
|
|
2238
|
+
if (s.visibility === "local" || s.metadata?.isLocalDefinition) continue;
|
|
2239
|
+
if (s.ownerName !== module) continue;
|
|
2240
|
+
s.qualifiedName = `${module}.${s.name}`;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
/** Apply the collected fixities to the operator symbols they name. */
|
|
2244
|
+
attachFixities(ctx) {
|
|
2245
|
+
if (ctx.fixities.length === 0) return;
|
|
2246
|
+
for (const f of ctx.fixities) {
|
|
2247
|
+
for (const s of ctx.symbols) {
|
|
2248
|
+
if (s.name !== f.name) continue;
|
|
2249
|
+
s.metadata = s.metadata ?? {};
|
|
2250
|
+
s.metadata.fixity = { associativity: f.associativity, precedence: f.precedence };
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Evidence tiers on EVERY relationship, and the lane's own provenance bit.
|
|
2256
|
+
*
|
|
2257
|
+
* M14 section D requires at least two distinct `evidenceClass` values pinned by
|
|
2258
|
+
* fixtures, and the family vocabulary is fixed: `structural`,
|
|
2259
|
+
* `usage_contextual`, `import_based`, `reference_qualified`, `inferred` (the
|
|
2260
|
+
* typescript and haskell classifiers are the precedents). Clean uses only those
|
|
2261
|
+
* five, so the values mean what they mean in every other lane.
|
|
2262
|
+
*
|
|
2263
|
+
* | kind | tier | why |
|
|
2264
|
+
* | --- | --- | --- |
|
|
2265
|
+
* | `contains`, `exports` | `structural` | both endpoints are names this file publishes, and the store's own resolution proves it |
|
|
2266
|
+
* | `imports` | `import_based` | the source spells the module and the item |
|
|
2267
|
+
* | `instantiates`, `uses` | `structural` | in-file by construction — the target must be a `data_constructor`, record field or local this file declares, or no edge is emitted |
|
|
2268
|
+
* | `implements` | `structural` in-file, `reference_qualified` external | an `instance C T` is a structural fact when `C` is declared here; when `C` is imported the edge names a class by reference, which is 322 of 782 corpus sites |
|
|
2269
|
+
* | `references` | `reference_qualified` | the family rule: a type reference names its type |
|
|
2270
|
+
* | `calls` | `usage_contextual`, or `reference_qualified` for a source-spelled qualified target | a call is usage evidence; a call to `'Data.Error'.Ok` or `M.lookup` is identified by the spelling in the source, not by a same-file binding |
|
|
2271
|
+
*
|
|
2272
|
+
* `evidenceClass` answers "how do we know this edge"; it cannot answer "could
|
|
2273
|
+
* this file have known the target at all", which is Clean's actual signature
|
|
2274
|
+
* property and the one the honesty gate measures. So `targetIsInFile` is
|
|
2275
|
+
* emitted alongside: `true` when the target resolves to a name this file
|
|
2276
|
+
* publishes (bare, `Owner.member`, or the leaf after the first `.` — the
|
|
2277
|
+
* store's own three strategies), `false` when the target is external by nature.
|
|
2278
|
+
* 7,777 of 9,568 corpus `references` and 322 of 782 `implements` are `false`,
|
|
2279
|
+
* and a consumer that wants only provable edges can now say so without
|
|
2280
|
+
* re-deriving it.
|
|
2281
|
+
*/
|
|
2282
|
+
classifyEvidenceTier(ctx) {
|
|
2283
|
+
const names = new Set(ctx.symbols.map((s) => s.name));
|
|
2284
|
+
const qNames = new Set(ctx.symbols.map((s) => s.qualifiedName).filter(Boolean));
|
|
2285
|
+
const resolves = (name) => {
|
|
2286
|
+
if (names.has(name) || qNames.has(name)) return true;
|
|
2287
|
+
const dot = name.indexOf(".");
|
|
2288
|
+
return dot > 0 && (names.has(name.slice(dot + 1)) || qNames.has(name.slice(dot + 1)));
|
|
2289
|
+
};
|
|
2290
|
+
for (const r of ctx.relationships) {
|
|
2291
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
2292
|
+
const t = String(r.type);
|
|
2293
|
+
const target = String(r.target);
|
|
2294
|
+
md.targetIsInFile = resolves(target);
|
|
2295
|
+
if (md.evidenceClass) continue;
|
|
2296
|
+
switch (t) {
|
|
2297
|
+
case "contains":
|
|
2298
|
+
case "exports":
|
|
2299
|
+
case "instantiates":
|
|
2300
|
+
case "uses":
|
|
2301
|
+
md.evidenceClass = "structural";
|
|
2302
|
+
continue;
|
|
2303
|
+
case "imports":
|
|
2304
|
+
md.evidenceClass = "import_based";
|
|
2305
|
+
continue;
|
|
2306
|
+
case "references":
|
|
2307
|
+
md.evidenceClass = "reference_qualified";
|
|
2308
|
+
continue;
|
|
2309
|
+
case "implements":
|
|
2310
|
+
md.evidenceClass = md.targetIsInFile ? "structural" : "reference_qualified";
|
|
2311
|
+
continue;
|
|
2312
|
+
default:
|
|
2313
|
+
md.evidenceClass = target.includes(".") || target.includes("'") ? "reference_qualified" : "usage_contextual";
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* MARK THE STANDARD ENVIRONMENT'S TYPE NAMES — one pass over the finished edge list, exactly as
|
|
2319
|
+
* kotlin's `markPlatformTargets` (`real-kotlin-analyzer.ts:411`) and go's stdlib marker do it, so
|
|
2320
|
+
* a new emission site cannot forget it and no pre-existing key is touched.
|
|
2321
|
+
*
|
|
2322
|
+
* THE PROBLEM IT ANSWERS, MEASURED ON THE WHOLE 312-FILE LANE. `unresolved` is 4,677 edges and
|
|
2323
|
+
* 2,540 of them are a BARE name: `String` 869, `World` 222, `Bool` 126, `Int` 90, `Real` 75,
|
|
2324
|
+
* `StdEnv` 56 … The declaration exists — in the Clean standard library, in another repository —
|
|
2325
|
+
* and this analyzer's own doctrine says why the edge is still emitted: "a `references` edge in a
|
|
2326
|
+
* typed position is a type reference whatever the type is" (this file's EDGE DISCIPLINE header).
|
|
2327
|
+
* What was missing was not the edge, it was the STATEMENT: 1,788 of those edges name a name the
|
|
2328
|
+
* standard environment declares, and the lane said nothing about it, so the arbiter counted every
|
|
2329
|
+
* one of them as a silent hole.
|
|
2330
|
+
*
|
|
2331
|
+
* THREE GUARDS, and each one is a fact about THIS file rather than a guess:
|
|
2332
|
+
*
|
|
2333
|
+
* 1. EDGE KIND. Only `references` and `implements` — the two edge types whose target IS a type
|
|
2334
|
+
* mention. `contains` / `exports` targets are this file's own declarations; `calls` targets
|
|
2335
|
+
* are callee leaves or the source-spelled qualified names the calls rule owns; `imports`
|
|
2336
|
+
* targets are `Module:item`, which cannot equal a bare name; `uses`, `overrides` and
|
|
2337
|
+
* `instantiates` carry 0 unresolved edges in the measured population (TABLE 1 of the report),
|
|
2338
|
+
* so marking them would be inert and is not done.
|
|
2339
|
+
* 2. THE CLOSED LIST. The target must be in `CLEAN_STD_ENV_NAMES`, which is DERIVED from the
|
|
2340
|
+
* corpus's own stdlib repositories (`clean-stdlib` + `Prelude`) — see that constant. A target
|
|
2341
|
+
* containing a dot is never in it, which matters: the arbiter refutes an `owner` claim whose
|
|
2342
|
+
* owner the project declares, and every dotted target has an owner segment that a project may
|
|
2343
|
+
* well publish (`System.CommandLine` in Clyde). Bare names only, so no claim this pass attaches
|
|
2344
|
+
* can be refuted, and `extFAIL` cannot move.
|
|
2345
|
+
* 3. THE FILE'S OWN DECLARATIONS VETO. `targetIsInFile` is computed by `classifyEvidenceTier`
|
|
2346
|
+
* over this file's symbol table and is already on every edge; where it is true the edge is a
|
|
2347
|
+
* same-file reference and there is nothing external to claim. Without this guard the claim
|
|
2348
|
+
* would be FALSE at the file's own scope, and the arbiter discloses exactly that as
|
|
2349
|
+
* `contradicted` in its claim-carrying-ambiguous block.
|
|
2350
|
+
*
|
|
2351
|
+
* NOTHING ELSE ABOUT AN EDGE IS TOUCHED: source, target, type, line and every pre-existing
|
|
2352
|
+
* metadata key are byte-identical, and the marker is read by the arbiter ONLY where the target
|
|
2353
|
+
* names nothing in the project's key space — so it can never move a resolved edge, can never
|
|
2354
|
+
* raise `resolved`, and never changes `collRes`.
|
|
2355
|
+
*/
|
|
2356
|
+
markStandardEnvironmentTargets(ctx) {
|
|
2357
|
+
for (const r of ctx.relationships) {
|
|
2358
|
+
const t = String(r.type);
|
|
2359
|
+
if (t !== ASTEngine.RelationshipType.References && t !== ASTEngine.RelationshipType.Implements) continue;
|
|
2360
|
+
const target = r.target;
|
|
2361
|
+
if (typeof target !== "string" || target.length === 0) continue;
|
|
2362
|
+
if (!CLEAN_STD_ENV_NAMES.has(target)) continue;
|
|
2363
|
+
const md = r.metadata;
|
|
2364
|
+
if (!md || typeof md !== "object") continue;
|
|
2365
|
+
if (md.targetIsInFile === true) continue;
|
|
2366
|
+
if (md.isStdlib === true) continue;
|
|
2367
|
+
md.isStdlib = true;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* A `type_signature` — Clean's declaration form, and where a function's
|
|
2372
|
+
* parameter types, result type, generic parameters, class context, special
|
|
2373
|
+
* types and inline code body all live. 3,611 of them published only a bare
|
|
2374
|
+
* Function symbol with no types at all before.
|
|
2375
|
+
*
|
|
2376
|
+
* The old handler published a symbol ONLY at module scope
|
|
2377
|
+
* (`scopeStack.length === 0`) and then `continue`d, which meant a class member's
|
|
2378
|
+
* signature got its Method from a separate scan inside `processClass`, an
|
|
2379
|
+
* instance member's signature published nothing at all (only its
|
|
2380
|
+
* `function_declaration` / `operator_definition` sibling did), and a where-local
|
|
2381
|
+
* signature was discarded. All three publish through this one handler now.
|
|
2382
|
+
*/
|
|
2383
|
+
processTypeSignature(node, ctx, currentScope, inErrorRegion = false) {
|
|
2384
|
+
const name = this.declarationName(node);
|
|
2385
|
+
if (!name) return;
|
|
2386
|
+
const isLocal = this.isLocalScope(currentScope);
|
|
2387
|
+
const isMember = currentScope?.kind === ASTEngine.SymbolKind.Interface || currentScope?.kind === ASTEngine.SymbolKind.Class;
|
|
2388
|
+
const key = declarationKey(ctx.currentFrame, this.ownerIdentity(currentScope, ctx), name);
|
|
2389
|
+
const info = ctx.signaturesByNode.get(node.id) ?? ctx.signatures.get(key);
|
|
2390
|
+
const existingIdx = ctx.functionSymbolIndex[key];
|
|
2391
|
+
if (existingIdx !== void 0) {
|
|
2392
|
+
if (process.env.BB_CLEAN_DIAG) process.stderr.write(`DIAG apply L${this.lineOf(node)} sym=${ctx.symbols[existingIdx].name} key=${key} merge info=${info ? "Y" : "N"}
|
|
2393
|
+
`);
|
|
2394
|
+
if (info) this.applySignature(ctx.symbols[existingIdx], info, ctx, isLocal);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const kind = this.callableKind(currentScope, isLocal);
|
|
2398
|
+
const sym = {
|
|
2399
|
+
name,
|
|
2400
|
+
kind,
|
|
2401
|
+
range: this.nodeToRange(node),
|
|
2402
|
+
scope: currentScope?.name,
|
|
2403
|
+
...isLocal ? { qualifiedName: ctx.currentFrame === void 0 ? name : `${ctx.currentFrame}.${name}` } : {},
|
|
2404
|
+
ownerName: isLocal ? this.localOwner(ctx) : currentScope?.name || ctx.currentModule,
|
|
2405
|
+
ownerKind: isLocal ? ASTEngine.SymbolKind.Function : currentScope?.kind,
|
|
2406
|
+
// A callable reached only through an ERROR region has no knowable scope, so it
|
|
2407
|
+
// is not part of the module's API and must not claim to be exported.
|
|
2408
|
+
visibility: isLocal || inErrorRegion ? "local" : "public",
|
|
2409
|
+
// A member whose only occurrence is a signature is an ABSTRACT member: the
|
|
2410
|
+
// class declares it and no instance in this file defines it.
|
|
2411
|
+
isAbstract: isMember ? true : void 0,
|
|
2412
|
+
metadata: {
|
|
2413
|
+
cleanSpecific: true,
|
|
2414
|
+
isDeclaration: true,
|
|
2415
|
+
isLocalDeclaration: isLocal || void 0
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
this.attachComments(sym, node);
|
|
2419
|
+
ctx.symbols.push(sym);
|
|
2420
|
+
ctx.functionSymbolIndex[key] = ctx.symbols.length - 1;
|
|
2421
|
+
if (info) {
|
|
2422
|
+
if (process.env.BB_CLEAN_DIAG) process.stderr.write(`DIAG apply L${this.lineOf(info.node)} sym=${sym.name} key=${key} fresh
|
|
2423
|
+
`);
|
|
2424
|
+
this.applySignature(sym, info, ctx, isLocal);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
/** Copy a `SignatureInfo` onto the symbol it declares, and emit its edges. */
|
|
2428
|
+
applySignature(sym, info, ctx, isLocal) {
|
|
2429
|
+
const md = sym.metadata ?? (sym.metadata = {});
|
|
2430
|
+
if (info.returnType && !sym.returnType) sym.returnType = info.returnType;
|
|
2431
|
+
if (info.genericVars.size > 0) {
|
|
2432
|
+
const existing = sym.genericParameters ?? [];
|
|
2433
|
+
for (const g of info.genericVars) if (!existing.includes(g)) existing.push(g);
|
|
2434
|
+
sym.genericParameters = existing;
|
|
2435
|
+
}
|
|
2436
|
+
if (md.typeSignature === void 0) md.typeSignature = info.node.text;
|
|
2437
|
+
if (info.specialTypes.length > 0) {
|
|
2438
|
+
md.specialTypes = info.specialTypes;
|
|
2439
|
+
ctx.patterns["Specialised Members"] = true;
|
|
2440
|
+
}
|
|
2441
|
+
if (info.hasCodeBody) {
|
|
2442
|
+
md.hasInlineCodeBody = true;
|
|
2443
|
+
ctx.patterns["Inline Machine Code"] = true;
|
|
2444
|
+
}
|
|
2445
|
+
if (info.fixity) {
|
|
2446
|
+
md.fixity = info.fixity;
|
|
2447
|
+
ctx.patterns["Operator Fixity Declarations"] = true;
|
|
2448
|
+
ctx.fixities.push({ name: sym.name, ...info.fixity, line: this.lineOf(info.node) });
|
|
2449
|
+
}
|
|
2450
|
+
const source = isLocal ? md.scopedName ?? sym.name : sym.name;
|
|
2451
|
+
if (info.contextClasses.length > 0) {
|
|
2452
|
+
md.classContext = info.contextClasses;
|
|
2453
|
+
ctx.patterns["Class Contexts"] = true;
|
|
2454
|
+
const line = this.lineOf(info.node.childForFieldName("context"));
|
|
2455
|
+
for (const c of info.contextClasses) {
|
|
2456
|
+
this.pushRel(ctx, source, c, ASTEngine.RelationshipType.References, 0.95, {
|
|
2457
|
+
cleanSpecific: true,
|
|
2458
|
+
relationshipType: "class_context"
|
|
2459
|
+
}, line);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
for (const ref of info.typeRefs) {
|
|
2463
|
+
this.pushRel(ctx, source, ref.name, ASTEngine.RelationshipType.References, 0.95, {
|
|
2464
|
+
cleanSpecific: true,
|
|
2465
|
+
relationshipType: "declared_type"
|
|
2466
|
+
}, this.lineOf(ref.node));
|
|
2467
|
+
}
|
|
2468
|
+
md.paramTypes = info.paramTypes;
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* The `special` block of a signature: `special a=Int; a=Char`.
|
|
2472
|
+
*
|
|
2473
|
+
* 104 blocks / 507 members corpus-wide, published nothing before. A `special`
|
|
2474
|
+
* member does not declare a new NAME — the variable is the enclosing
|
|
2475
|
+
* declaration's own type parameter (`_from :: a -> .[a] | Enum, Ord a` with
|
|
2476
|
+
* `special a=Int` specialises `_from`) — so it publishes no symbol. What it does
|
|
2477
|
+
* declare is a set of TYPES the enclosing callable is specialised for, and
|
|
2478
|
+
* those are real `references` edges.
|
|
2479
|
+
*/
|
|
2480
|
+
processSpecialBlock(node, ctx) {
|
|
2481
|
+
const owner = this.relationshipSource(ctx) ?? this.enclosingDeclarationName(node);
|
|
2482
|
+
if (!owner) return;
|
|
2483
|
+
ctx.patterns["Specialised Members"] = true;
|
|
2484
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2485
|
+
const m = node.namedChild(i);
|
|
2486
|
+
if (!m || m.type !== "special_member") continue;
|
|
2487
|
+
const t = m.childForFieldName("type");
|
|
2488
|
+
if (!t) continue;
|
|
2489
|
+
for (const ref of this.typeRefsIn(t, ctx)) {
|
|
2490
|
+
this.pushRel(ctx, owner, ref.name, ASTEngine.RelationshipType.References, 0.95, {
|
|
2491
|
+
cleanSpecific: true,
|
|
2492
|
+
relationshipType: "special_type"
|
|
2493
|
+
}, this.lineOf(ref.node));
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Join a `type_signature`'s parameter TYPES to the definition's parameter NAMES.
|
|
2499
|
+
*
|
|
2500
|
+
* This is the join that makes Clean parameter data real. A Clean signature
|
|
2501
|
+
* `comp3 :: !(.a -> .b) u:(.c -> .a) !(.d -> .c) -> u:(.d -> .b)` yields three
|
|
2502
|
+
* domain types; the definition `comp3 f g h` yields three names; pairing them
|
|
2503
|
+
* positionally is sound because a Clean function type's domain is a
|
|
2504
|
+
* left-nested `type_application` whose children are in parameter order, and a
|
|
2505
|
+
* definition's `pattern` fields are its parameters in order — including the
|
|
2506
|
+
* multi-guard shape `f x # g y = …`, where the guard equations share the
|
|
2507
|
+
* parameter list and the patterns are collected across all of them, first
|
|
2508
|
+
* occurrence winning.
|
|
2509
|
+
*/
|
|
2510
|
+
joinParameters(sym, defNode, ctx, owner) {
|
|
2511
|
+
if (sym.parameters && sym.parameters.length > 0) return;
|
|
2512
|
+
const key = declarationKey(ctx.currentFrame, owner, sym.name);
|
|
2513
|
+
const info = ctx.signatures.get(key);
|
|
2514
|
+
if (!info) return;
|
|
2515
|
+
const params = [];
|
|
2516
|
+
let i = 0;
|
|
2517
|
+
for (const p of this.fields(defNode, "pattern")) {
|
|
2518
|
+
const name = this.patternParameterName(p);
|
|
2519
|
+
params.push({ name, type: info.paramTypes[i], isOptional: name === OPTIONAL_PARAM_NAME });
|
|
2520
|
+
i++;
|
|
2521
|
+
}
|
|
2522
|
+
if (params.length > 0) sym.parameters = params;
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* The NAME a parameter pattern binds, or the placeholder for a pattern that
|
|
2526
|
+
* binds none.
|
|
2527
|
+
*
|
|
2528
|
+
* ONE `pattern` field is ONE parameter. That is the rule, and the first
|
|
2529
|
+
* implementation got it wrong in a way the fixture caught: it collected the
|
|
2530
|
+
* names bound INSIDE each pattern, so `addBid (Book bids asks) price` produced
|
|
2531
|
+
* three parameters (`bids`, `asks`, `price`) while the signature
|
|
2532
|
+
* `OrderBook Int -> OrderBook` has two domain types — a tuple pattern is one
|
|
2533
|
+
* parameter of a tuple type, not three parameters.
|
|
2534
|
+
*
|
|
2535
|
+
* A pattern that names its target (`identifier`, or `strict_binding_pattern`'s
|
|
2536
|
+
* `p=:t` left side) yields that name. A destructuring pattern
|
|
2537
|
+
* (`constructor_pattern`, `list_pattern`, `tuple_pattern`, `record_pattern`,
|
|
2538
|
+
* `unboxed_pattern`, `cons_pattern`, `wildcard`, `record_update_pattern`)
|
|
2539
|
+
* binds no single target, so the parameter is anonymous and is named by the
|
|
2540
|
+
* constant below rather than by inventing a name: the store keys a parameter by
|
|
2541
|
+
* name, so a fabricated one would collide across overloads, while a reserved
|
|
2542
|
+
* placeholder cannot.
|
|
2543
|
+
*/
|
|
2544
|
+
patternParameterName(pat) {
|
|
2545
|
+
switch (pat.type) {
|
|
2546
|
+
case "identifier":
|
|
2547
|
+
return pat.text;
|
|
2548
|
+
case "strict_binding_pattern": {
|
|
2549
|
+
const first = pat.namedChild(0);
|
|
2550
|
+
if (first?.type === "identifier") return first.text;
|
|
2551
|
+
return this.condensePattern(pat);
|
|
2552
|
+
}
|
|
2553
|
+
case "strict_pattern": {
|
|
2554
|
+
const first = pat.namedChild(0);
|
|
2555
|
+
return first?.type === "identifier" ? first.text : this.condensePattern(pat);
|
|
2556
|
+
}
|
|
2557
|
+
case "wildcard":
|
|
2558
|
+
return OPTIONAL_PARAM_NAME;
|
|
2559
|
+
default:
|
|
2560
|
+
return this.condensePattern(pat);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* A destructuring pattern as a one-line name. The pattern's own text is the
|
|
2565
|
+
* name the source gives that parameter slot, so it is used verbatim; only
|
|
2566
|
+
* NEWLINES and RUNS OF SPACES are folded, because a name that carries a
|
|
2567
|
+
* newline is not a name. Folding is a character operation (indexOf/slice over
|
|
2568
|
+
* the two whitespace characters), not a pattern match.
|
|
2569
|
+
*/
|
|
2570
|
+
condensePattern(pat) {
|
|
2571
|
+
const raw = pat.text;
|
|
2572
|
+
let out = "";
|
|
2573
|
+
let pendingSpace = false;
|
|
2574
|
+
for (let i = 0; i < raw.length; i++) {
|
|
2575
|
+
const code = raw.charCodeAt(i);
|
|
2576
|
+
const isSpace = code === 32 || code === 9 || code === 10 || code === 13;
|
|
2577
|
+
if (isSpace) {
|
|
2578
|
+
pendingSpace = out.length > 0;
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (pendingSpace) {
|
|
2582
|
+
out += " ";
|
|
2583
|
+
pendingSpace = false;
|
|
2584
|
+
}
|
|
2585
|
+
out += raw[i];
|
|
2586
|
+
}
|
|
2587
|
+
return out || OPTIONAL_PARAM_NAME;
|
|
2588
|
+
}
|
|
2589
|
+
// ============================================================================
|
|
2590
|
+
// PASS 2 — expression-level extraction
|
|
2591
|
+
// ============================================================================
|
|
2592
|
+
/**
|
|
2593
|
+
* An application: the lane's call / instantiation / qualified-call surface.
|
|
2594
|
+
*
|
|
2595
|
+
* The pinned grammar makes an application LEFT-NESTED: `f a b c` is
|
|
2596
|
+
* `application(application(application(f, a), b), c)`, so only the INNERMOST
|
|
2597
|
+
* application carries the callee. Measured over 23,908 applications, the
|
|
2598
|
+
* `function` field is 13,102 `application` (the nest), 8,340 `identifier`, 2,045
|
|
2599
|
+
* `constructor`, 162 `field_access`, 78 `single_quoted_name`, 61
|
|
2600
|
+
* `unboxed_expression`, 25 `parenthesized_operator`, 25 `number`, 22 `string`,
|
|
2601
|
+
* 20 `paren_expression`, 14 `index_access`, and 15 across five more shapes. The
|
|
2602
|
+
* old handler recognised only `identifier` and `constructor` (plus the corpus-dead
|
|
2603
|
+
* `qualified_identifier`), so the 265 `field_access` / `single_quoted_name` /
|
|
2604
|
+
* `parenthesized_operator` heads produced no edge at all.
|
|
2605
|
+
*/
|
|
2606
|
+
extractApplication(node, ctx, scopeStack) {
|
|
2607
|
+
const head = node.childForFieldName("function");
|
|
2608
|
+
if (!head) return;
|
|
2609
|
+
const owner = this.relationshipSource(ctx);
|
|
2610
|
+
if (!owner) return;
|
|
2611
|
+
switch (head.type) {
|
|
2612
|
+
case "identifier": {
|
|
2613
|
+
const target = this.resolveCallableTarget(head.text, scopeStack, ctx);
|
|
2614
|
+
if (!target) return;
|
|
2615
|
+
this.pushRel(ctx, owner, target, ASTEngine.RelationshipType.Calls, 0.9, {
|
|
2616
|
+
cleanSpecific: true,
|
|
2617
|
+
context: "local function call"
|
|
2618
|
+
}, this.lineOf(head));
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
case "constructor": {
|
|
2622
|
+
if (this.isPatternPosition(node)) return;
|
|
2623
|
+
const name = head.text;
|
|
2624
|
+
if (ctx.declaredConstructors.has(name)) {
|
|
2625
|
+
this.pushRel(ctx, owner, name, ASTEngine.RelationshipType.Instantiates, 1, {
|
|
2626
|
+
cleanSpecific: true,
|
|
2627
|
+
isConstructorApplication: true,
|
|
2628
|
+
// A `data_constructor` node in THIS file proves the target is a data
|
|
2629
|
+
// constructor, so this edge carries no inference at all.
|
|
2630
|
+
isExternalTarget: false
|
|
2631
|
+
}, this.lineOf(head));
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (ctx.declaredFunctions.has(name)) {
|
|
2635
|
+
const target = this.resolveCallableTarget(name, scopeStack, ctx);
|
|
2636
|
+
if (target) {
|
|
2637
|
+
this.pushRel(ctx, owner, target, ASTEngine.RelationshipType.Calls, 0.9, {
|
|
2638
|
+
cleanSpecific: true,
|
|
2639
|
+
context: "uppercase function application"
|
|
2640
|
+
}, this.lineOf(head));
|
|
2641
|
+
}
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
if (ctx.declaredClassNames.has(name)) {
|
|
2645
|
+
this.pushRel(ctx, owner, name, ASTEngine.RelationshipType.Calls, 0.9, {
|
|
2646
|
+
cleanSpecific: true,
|
|
2647
|
+
context: "class method application",
|
|
2648
|
+
isClassMethodCall: true
|
|
2649
|
+
}, this.lineOf(head));
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
if (ctx.declaredTypeDefNames.has(name)) {
|
|
2653
|
+
ctx.typeCoercions.count++;
|
|
2654
|
+
if (ctx.typeCoercions.names.length < 25 && !ctx.typeCoercions.names.includes(name)) {
|
|
2655
|
+
ctx.typeCoercions.names.push(name);
|
|
2656
|
+
}
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
ctx.declinedInstantiations.count++;
|
|
2660
|
+
if (ctx.declinedInstantiations.names.length < 25 && !ctx.declinedInstantiations.names.includes(name)) {
|
|
2661
|
+
ctx.declinedInstantiations.names.push(name);
|
|
2662
|
+
}
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
case "single_quoted_name":
|
|
2666
|
+
case "qualified_identifier": {
|
|
2667
|
+
const target = head.type === "single_quoted_name" ? this.qualifiedTarget(head.text) : head.text;
|
|
2668
|
+
if (!target) return;
|
|
2669
|
+
this.pushRel(ctx, owner, target, ASTEngine.RelationshipType.Calls, 0.9, {
|
|
2670
|
+
cleanSpecific: true,
|
|
2671
|
+
isQualifiedCall: true,
|
|
2672
|
+
qualifier: this.qualifierOf(head.text)
|
|
2673
|
+
}, this.lineOf(head));
|
|
2674
|
+
if (ctx.currentFrame) {
|
|
2675
|
+
const idx = ctx.frameSymbolIndex.get(ctx.currentFrame);
|
|
2676
|
+
const sym = idx === void 0 ? void 0 : ctx.symbols[idx];
|
|
2677
|
+
if (sym) {
|
|
2678
|
+
const list = sym.qualified_calls ?? (sym.qualified_calls = []);
|
|
2679
|
+
if (!list.includes(target)) list.push(target);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
case "field_access": {
|
|
2685
|
+
this.emitFieldUse(head, ctx, "method_call");
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
case "parenthesized_operator": {
|
|
2689
|
+
const name = this.unparenthesize(head.text);
|
|
2690
|
+
if (!name) return;
|
|
2691
|
+
if (ctx.declaredFunctions.has(name) || ctx.declaredTypeNames.has(name)) {
|
|
2692
|
+
this.pushRel(ctx, owner, name, ASTEngine.RelationshipType.Calls, 0.9, {
|
|
2693
|
+
cleanSpecific: true,
|
|
2694
|
+
context: "prefix operator application"
|
|
2695
|
+
}, this.lineOf(head));
|
|
2696
|
+
}
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
default:
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* `obj.field` — a record field read. 1,129 sites corpus-wide, published nothing
|
|
2705
|
+
* before. The `field` field carries the field NAME (1,058 `identifier`, 55
|
|
2706
|
+
* `constructor` — a field may be uppercase, as in `d.Diagnostic.severity` — and
|
|
2707
|
+
* 16 `single_quoted_name`).
|
|
2708
|
+
*
|
|
2709
|
+
* Emitted only when the name is a record field THIS FILE declares (the
|
|
2710
|
+
* FP-family rule, applied to property access as to calls): the receiver's type
|
|
2711
|
+
* is not resolvable inside one file, so an unguarded edge to a name that may be
|
|
2712
|
+
* a local variable would be a fabrication. `uses` is the type — a field read is
|
|
2713
|
+
* a use of the field, not a call of it.
|
|
2714
|
+
*/
|
|
2715
|
+
emitFieldUse(access, ctx, label) {
|
|
2716
|
+
const owner = this.relationshipSource(ctx);
|
|
2717
|
+
if (!owner) return;
|
|
2718
|
+
const field = access.childForFieldName("field");
|
|
2719
|
+
if (!field) return;
|
|
2720
|
+
const name = field.text;
|
|
2721
|
+
if (!ctx.declaredFieldNames.has(name)) return;
|
|
2722
|
+
this.pushRel(ctx, owner, name, ASTEngine.RelationshipType.Uses, 0.9, {
|
|
2723
|
+
cleanSpecific: true,
|
|
2724
|
+
relationshipType: label,
|
|
2725
|
+
isRecordField: true
|
|
2726
|
+
}, this.lineOf(field));
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* `{base & f = v, g = w}` — a record update. 469 sites, 793 `update_field`s.
|
|
2730
|
+
*
|
|
2731
|
+
* The `field` field points at an `update_field` and the `value` fields
|
|
2732
|
+
* interleave with it, so the field NAMES are read from the `update_field`
|
|
2733
|
+
* children (`field → identifier`) rather than by zipping two parallel field
|
|
2734
|
+
* lists. Measured: field count equals value count on all 469 (`f1v1` 288,
|
|
2735
|
+
* `f2v2` 90, `f3v3` 66, `f4v4` 14, `f5v5` 7, `f6v6` 2, `f11v11` 1,
|
|
2736
|
+
* `f12v12` 1), so a positional zip would work today, but the names are on the
|
|
2737
|
+
* nodes and reading them there is the construction that cannot break.
|
|
2738
|
+
*/
|
|
2739
|
+
extractRecordUpdate(node, ctx) {
|
|
2740
|
+
const owner = this.relationshipSource(ctx);
|
|
2741
|
+
if (!owner) return;
|
|
2742
|
+
for (const f of this.fields(node, "field")) {
|
|
2743
|
+
const inner = f.childForFieldName("field") ?? f.namedChild(0);
|
|
2744
|
+
if (!inner) continue;
|
|
2745
|
+
const name = inner.text;
|
|
2746
|
+
if (!ctx.declaredFieldNames.has(name)) continue;
|
|
2747
|
+
this.pushRel(ctx, owner, name, ASTEngine.RelationshipType.Uses, 0.9, {
|
|
2748
|
+
cleanSpecific: true,
|
|
2749
|
+
relationshipType: "record_field_update",
|
|
2750
|
+
isRecordField: true
|
|
2751
|
+
}, this.lineOf(inner));
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* `{a = 1, b = 2}` — a record CONSTRUCTION. 399 sites, published nothing before.
|
|
2756
|
+
* Every field the literal sets is a `uses` of that field.
|
|
2757
|
+
*/
|
|
2758
|
+
extractRecordExpression(node, ctx) {
|
|
2759
|
+
const owner = this.relationshipSource(ctx);
|
|
2760
|
+
if (!owner) return;
|
|
2761
|
+
for (const n of this.fields(node, "name")) {
|
|
2762
|
+
const name = n.text;
|
|
2763
|
+
if (!ctx.declaredFieldNames.has(name)) continue;
|
|
2764
|
+
this.pushRel(ctx, owner, name, ASTEngine.RelationshipType.Uses, 0.9, {
|
|
2765
|
+
cleanSpecific: true,
|
|
2766
|
+
relationshipType: "record_construction",
|
|
2767
|
+
isRecordField: true
|
|
2768
|
+
}, this.lineOf(n));
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* `Just x` in PATTERN position — a match on a data constructor. 1,240 sites,
|
|
2773
|
+
* published nothing before.
|
|
2774
|
+
*
|
|
2775
|
+
* This is deliberately NOT an `instantiates`: `case x of Just y -> …` does not
|
|
2776
|
+
* build a `Just`, it depends on one. `uses` is the type, guarded by the same
|
|
2777
|
+
* in-file declaration rule as every other edge, and labelled so the graph can
|
|
2778
|
+
* tell a match from a read or a write.
|
|
2779
|
+
*/
|
|
2780
|
+
extractConstructorPattern(node, ctx) {
|
|
2781
|
+
const owner = this.relationshipSource(ctx);
|
|
2782
|
+
if (!owner) return;
|
|
2783
|
+
for (const c of this.fields(node, "constructor")) {
|
|
2784
|
+
if (c.type !== "constructor") continue;
|
|
2785
|
+
if (!ctx.declaredConstructors.has(c.text)) continue;
|
|
2786
|
+
this.pushRel(ctx, owner, c.text, ASTEngine.RelationshipType.Uses, 0.9, {
|
|
2787
|
+
cleanSpecific: true,
|
|
2788
|
+
relationshipType: "pattern_match",
|
|
2789
|
+
isPatternMatch: true
|
|
2790
|
+
}, this.lineOf(c));
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Resolve a bare callee name to a target: the innermost visible LOCAL wins
|
|
2795
|
+
* (Clean scoping — a where-binder shadows the module binding for the whole RHS
|
|
2796
|
+
* of its callable), else the module-level declared set, else no edge.
|
|
2797
|
+
*
|
|
2798
|
+
* The walk order makes the local win correct without a second rule: a
|
|
2799
|
+
* `where_block` pushes its `locals` before the walk descends into the
|
|
2800
|
+
* definitions' bodies, and pops on exit, so an application in a guard equation
|
|
2801
|
+
* resolves against the block that encloses it.
|
|
2802
|
+
*/
|
|
2803
|
+
resolveCallableTarget(name, scopeStack, ctx) {
|
|
2804
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
2805
|
+
const locals = scopeStack[i].locals;
|
|
2806
|
+
if (locals === void 0) continue;
|
|
2807
|
+
const hit = locals.get(name);
|
|
2808
|
+
if (hit !== void 0) return hit;
|
|
2809
|
+
}
|
|
2810
|
+
return ctx.declaredFunctions.has(name) ? name : void 0;
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* The symbol an expression-level edge is attributed to: the innermost open
|
|
2814
|
+
* frame's scoped identity, or the module when no frame is open. An application
|
|
2815
|
+
* outside any callable (a module-level initialiser) is attributed to the
|
|
2816
|
+
* module, which is where its symbol lives.
|
|
2817
|
+
*/
|
|
2818
|
+
relationshipSource(ctx) {
|
|
2819
|
+
return ctx.currentFrame ?? ctx.currentModule;
|
|
2820
|
+
}
|
|
2821
|
+
/** The name of the nearest enclosing declaration, for a non-frame position. */
|
|
2822
|
+
enclosingDeclarationName(node) {
|
|
2823
|
+
let p = node.parent;
|
|
2824
|
+
while (p) {
|
|
2825
|
+
if (CALLABLE_NODE_TYPES.has(p.type)) return this.callableDeclaredName(p);
|
|
2826
|
+
if (p.type === "class_declaration" || p.type === "instance_declaration" || p.type === "type_definition" || p.type === "generic_declaration") {
|
|
2827
|
+
return p.childForFieldName("name")?.text;
|
|
2828
|
+
}
|
|
2829
|
+
p = p.parent;
|
|
2830
|
+
}
|
|
2831
|
+
return void 0;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* `'Data.Map'.get` → `Data.Map.get`.
|
|
2835
|
+
*
|
|
2836
|
+
* The quote in a Clean qualified name wraps the MODULE, not the whole name:
|
|
2837
|
+
* the corpus form is `'Data.Error'.Ok` — `'Data.Error'` quoted, `.Ok` not — and
|
|
2838
|
+
* the alias form is `M.lookup`, unquoted. The first implementation of this
|
|
2839
|
+
* function assumed a fully-quoted name (remove one leading `'`, require one
|
|
2840
|
+
* trailing `'`), so it returned null for every real corpus site: measured, the
|
|
2841
|
+
* 78 `single_quoted_name` application heads in the corpus produced ZERO
|
|
2842
|
+
* qualified `calls` edges while the branch looked live.
|
|
2843
|
+
*
|
|
2844
|
+
* The normalization is therefore "drop the quote characters", which is a
|
|
2845
|
+
* character filter, and a result with no characters left is not a name.
|
|
2846
|
+
*/
|
|
2847
|
+
qualifiedTarget(text) {
|
|
2848
|
+
if (!text.startsWith("'")) return text || null;
|
|
2849
|
+
let out = "";
|
|
2850
|
+
for (let i = 0; i < text.length; i++) {
|
|
2851
|
+
const ch = text[i];
|
|
2852
|
+
if (ch !== "'") out += ch;
|
|
2853
|
+
}
|
|
2854
|
+
return out || null;
|
|
2855
|
+
}
|
|
2856
|
+
/** The qualifier (module or alias) part of a qualified name, for metadata. */
|
|
2857
|
+
qualifierOf(text) {
|
|
2858
|
+
const inner = this.qualifiedTarget(text);
|
|
2859
|
+
if (!inner) return void 0;
|
|
2860
|
+
const dot = inner.indexOf(".");
|
|
2861
|
+
return dot > 0 ? inner.slice(0, dot) : void 0;
|
|
2862
|
+
}
|
|
2863
|
+
// ============================================================================
|
|
2864
|
+
// PASS 3 — closure
|
|
2865
|
+
// ============================================================================
|
|
2866
|
+
/**
|
|
2867
|
+
* `contains` for owned symbols — module → members, type → constructors and
|
|
2868
|
+
* fields, class/instance → methods, local frame → its own definitions. The
|
|
2869
|
+
* CHILD's own naming line is the anchor that resolves the intended target among
|
|
2870
|
+
* same-named siblings (#921), and a self-named edge is skipped because a symbol
|
|
2871
|
+
* cannot contain itself.
|
|
2872
|
+
*/
|
|
2873
|
+
emitContainsEdges(ctx) {
|
|
2874
|
+
for (const s of ctx.symbols) {
|
|
2875
|
+
const owner = s.ownerName;
|
|
2876
|
+
if (owner && owner !== s.name) {
|
|
2877
|
+
this.pushRel(ctx, owner, s.name, ASTEngine.RelationshipType.Contains, 1, {}, s.range?.startLine);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Exports: every symbol the MODULE owns, plus an explicit `foreign export`.
|
|
2883
|
+
* Clean has no export keyword — a `.dcl`'s definition list IS the interface and
|
|
2884
|
+
* everything in it is public by default.
|
|
2885
|
+
*
|
|
2886
|
+
* Class/instance members keep their owner (`<class>` / the instance target type),
|
|
2887
|
+
* so they are NOT exported: their membership rides the `contains` edge, exactly
|
|
2888
|
+
* as the haskell lane treats a class method. Where/with/let locals are NOT
|
|
2889
|
+
* exported (M11.11 §2: a local is a real symbol but not part of the module's
|
|
2890
|
+
* surface) — now that locals publish at all, that exclusion is load-bearing
|
|
2891
|
+
* rather than incidental, and it is stated here because it is the one place a
|
|
2892
|
+
* `visibility: "local"` symbol could otherwise leak into the module interface.
|
|
2893
|
+
*
|
|
2894
|
+
* Name-keyed first-wins dedupe: the exports-family invariant (run-fixtures.ts,
|
|
2895
|
+
* M49 JVM-closure audit) requires UNIQUE export names, because a duplicate
|
|
2896
|
+
* cannot identify its provider. THE DEDUPE SURVIVES THE INSTANCE-CONTAINER FIX, and
|
|
2897
|
+
* the invariant is why: it is a property of the export LIST (one name, one
|
|
2898
|
+
* provider), not of any one declaration form, so it stays regardless of which
|
|
2899
|
+
* forms happen to collide this round. What the dedupe USED to absorb is gone at the
|
|
2900
|
+
* source — a file declaring `ParseState` and also implementing `instance Eq
|
|
2901
|
+
* ParseState` offered `ParseState` twice (59 of 312 files), and the instance
|
|
2902
|
+
* container is now `ParseState::instance` of kind `impl`, which is neither exported
|
|
2903
|
+
* nor a declaration of `ParseState`. Same dedupe as cpp/go/objc/clojure, keyed by
|
|
2904
|
+
* bare name — a Clean module exports a name, not a name+kind pair.
|
|
2905
|
+
*/
|
|
2906
|
+
emitExports(ctx) {
|
|
2907
|
+
const EXPORTABLE_KINDS = /* @__PURE__ */ new Set([
|
|
2908
|
+
ASTEngine.SymbolKind.Function,
|
|
2909
|
+
ASTEngine.SymbolKind.Type,
|
|
2910
|
+
ASTEngine.SymbolKind.Interface,
|
|
2911
|
+
ASTEngine.SymbolKind.Class,
|
|
2912
|
+
ASTEngine.SymbolKind.Module,
|
|
2913
|
+
ASTEngine.SymbolKind.Constructor,
|
|
2914
|
+
ASTEngine.SymbolKind.Property,
|
|
2915
|
+
ASTEngine.SymbolKind.Macro
|
|
2916
|
+
]);
|
|
2917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2918
|
+
const source = ctx.currentModule ?? "";
|
|
2919
|
+
for (const sym of ctx.symbols) {
|
|
2920
|
+
if (sym.name.startsWith("_")) continue;
|
|
2921
|
+
if (sym.name === ctx.currentModule) continue;
|
|
2922
|
+
if (!EXPORTABLE_KINDS.has(sym.kind)) continue;
|
|
2923
|
+
if (sym.visibility === "local" || sym.metadata?.isLocalDefinition) continue;
|
|
2924
|
+
if (seen.has(sym.name)) continue;
|
|
2925
|
+
if (sym.ownerName !== ctx.currentModule && sym.ownerName !== void 0) continue;
|
|
2926
|
+
seen.add(sym.name);
|
|
2927
|
+
ctx.exports.push({ name: sym.name, kind: sym.kind, range: sym.range, visibility: "public" });
|
|
2928
|
+
if (source) {
|
|
2929
|
+
this.pushRel(
|
|
2930
|
+
ctx,
|
|
2931
|
+
source,
|
|
2932
|
+
sym.name,
|
|
2933
|
+
ASTEngine.RelationshipType.Exports,
|
|
2934
|
+
1,
|
|
2935
|
+
{ cleanSpecific: true },
|
|
2936
|
+
sym.range?.startLine
|
|
2937
|
+
);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
// ============================================================================
|
|
2942
|
+
// PASS 3 — plumbing
|
|
2943
|
+
// ============================================================================
|
|
2944
|
+
/**
|
|
2945
|
+
* Relationship emission, deduped by source|type|target|line identity.
|
|
2946
|
+
*
|
|
2947
|
+
* Dimension 8 / M59.O2: an edge carries the SITE LINE it happened on — 1-based,
|
|
2948
|
+
* on the name-bearing node, per-site and not per-set, because navigation is the
|
|
2949
|
+
* product. Two consequences, both deliberate:
|
|
2950
|
+
* * the line is part of the DEDUPE KEY (the clojure lane's rule,
|
|
2951
|
+
* real-clojure-analyzer.ts:1388): a function called from three lines is
|
|
2952
|
+
* three edges at three lines, not one edge at whichever line the walk saw
|
|
2953
|
+
* first. The store already keys its rows the same way
|
|
2954
|
+
* (parallel-indexer.ts:4439), and for a `contains` edge the child's own
|
|
2955
|
+
* naming line is what resolves the intended target among same-named
|
|
2956
|
+
* siblings (#921);
|
|
2957
|
+
* * a line that is not a positive integer is recorded as ABSENT, never as 0.
|
|
2958
|
+
* M59.O2: the contract is "the site line or NULL" — a fabricated 0 reads as
|
|
2959
|
+
* "line 0" to every reader downstream.
|
|
2960
|
+
*/
|
|
2961
|
+
pushRel(ctx, source, target, type, confidence, metadata = {}, line) {
|
|
2962
|
+
if (!source || !target) return;
|
|
2963
|
+
const hasLine = typeof line === "number" && Number.isInteger(line) && line > 0;
|
|
2964
|
+
const key = `${source}|${type}|${target}|${hasLine ? line : ""}`;
|
|
2965
|
+
if (ctx.seenRels.has(key)) return;
|
|
2966
|
+
ctx.seenRels.add(key);
|
|
2967
|
+
ctx.relationships.push({
|
|
2968
|
+
source,
|
|
2969
|
+
target,
|
|
2970
|
+
type,
|
|
2971
|
+
confidence,
|
|
2972
|
+
metadata,
|
|
2973
|
+
...hasLine ? { line_number: line } : {}
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
/** 1-based line of a node, or undefined — never 0 (M59.O2). */
|
|
2977
|
+
lineOf(node) {
|
|
2978
|
+
if (!node) return void 0;
|
|
2979
|
+
const line = node.startPosition.row + 1;
|
|
2980
|
+
return line > 0 ? line : void 0;
|
|
2981
|
+
}
|
|
2982
|
+
// ============================================================================
|
|
2983
|
+
// PASS 2 — shared helpers
|
|
2984
|
+
// ============================================================================
|
|
2985
|
+
isLocalScope(scope) {
|
|
2986
|
+
return scope !== void 0 && scope.kind === ASTEngine.SymbolKind.Namespace && scope.name === LOCAL_SCOPE_NAME;
|
|
2987
|
+
}
|
|
2988
|
+
/**
|
|
2989
|
+
* The identity a declaration in `scope` merges under: the local's enclosing
|
|
2990
|
+
* FRAME when the scope is a where/with/let block (that is what keeps a local
|
|
2991
|
+
* from colliding with a module callable), otherwise the scope's own name.
|
|
2992
|
+
*/
|
|
2993
|
+
ownerIdentity(scope, ctx) {
|
|
2994
|
+
if (this.isLocalScope(scope)) return ctx.currentFrame;
|
|
2995
|
+
return scope?.name;
|
|
2996
|
+
}
|
|
2997
|
+
/** A callable's kind from the scope it is declared in. */
|
|
2998
|
+
callableKind(scope, isLocal) {
|
|
2999
|
+
if (isLocal) return ASTEngine.SymbolKind.Function;
|
|
3000
|
+
if (scope === void 0) return ASTEngine.SymbolKind.Function;
|
|
3001
|
+
if (scope.kind === ASTEngine.SymbolKind.Interface || scope.kind === ASTEngine.SymbolKind.Class) {
|
|
3002
|
+
return ASTEngine.SymbolKind.Method;
|
|
3003
|
+
}
|
|
3004
|
+
if (scope.kind === ASTEngine.SymbolKind.Type) return ASTEngine.SymbolKind.Property;
|
|
3005
|
+
return ASTEngine.SymbolKind.Function;
|
|
3006
|
+
}
|
|
3007
|
+
/** The `| C a, D b` constraints of a class or instance, as edges. */
|
|
3008
|
+
emitContextRefs(context, source, label, ctx) {
|
|
3009
|
+
const info = {
|
|
3010
|
+
node: context,
|
|
3011
|
+
paramTypes: [],
|
|
3012
|
+
genericVars: /* @__PURE__ */ new Set(),
|
|
3013
|
+
typeRefs: [],
|
|
3014
|
+
contextClasses: [],
|
|
3015
|
+
specialTypes: [],
|
|
3016
|
+
hasCodeBody: false
|
|
3017
|
+
};
|
|
3018
|
+
this.readClassContext(context, info);
|
|
3019
|
+
if (info.contextClasses.length === 0) return;
|
|
3020
|
+
ctx.patterns["Class Contexts"] = true;
|
|
3021
|
+
for (const c of info.contextClasses) {
|
|
3022
|
+
this.pushRel(ctx, source, c, ASTEngine.RelationshipType.References, 0.95, {
|
|
3023
|
+
cleanSpecific: true,
|
|
3024
|
+
relationshipType: label
|
|
3025
|
+
}, this.lineOf(context));
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Type names mentioned by a type expression, as `references` edges — Clean's
|
|
3030
|
+
* `extractDeclaredTypeRefs` (the TypeScript lane's, at
|
|
3031
|
+
* real-typescript-analyzer.ts:3144). The line is the NAME's own line, not the
|
|
3032
|
+
* type expression's start, because a multi-line type puts its arguments on
|
|
3033
|
+
* later lines and the reader navigates to the name.
|
|
3034
|
+
*/
|
|
3035
|
+
emitTypeRefs(typeNode, source, label, ctx) {
|
|
3036
|
+
for (const ref of this.typeRefsIn(typeNode, ctx)) {
|
|
3037
|
+
this.pushRel(ctx, source, ref.name, ASTEngine.RelationshipType.References, 0.95, {
|
|
3038
|
+
cleanSpecific: true,
|
|
3039
|
+
relationshipType: label
|
|
3040
|
+
}, this.lineOf(ref.node));
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* The declared name of a function/operator definition, read from the grammar's
|
|
3045
|
+
* FIELD.
|
|
3046
|
+
*
|
|
3047
|
+
* `findChild(node, n => n.type === "identifier")` returns the FIRST identifier
|
|
3048
|
+
* child, which is only the name while the name happens to be typed `identifier`.
|
|
3049
|
+
* An uppercase definition's name is typed `constructor` (Clean lets a constructor
|
|
3050
|
+
* be a function and the grammar says so), so the heuristic skipped over it and
|
|
3051
|
+
* published the first PARAMETER as the function name. Measured over this lane's
|
|
3052
|
+
* corpora, per grammar: pinned v1.2.4 — 4,607 declarations, 452 published a
|
|
3053
|
+
* parameter as the name, 252 dropped; v1.2.7 — 4,647 declarations, 447 published
|
|
3054
|
+
* a parameter, 244 dropped. The field is exact, so it is used when present and
|
|
3055
|
+
* the scan remains only as a fallback for a node with no field.
|
|
3056
|
+
*/
|
|
3057
|
+
declaredName(node) {
|
|
3058
|
+
const byField = node.childForFieldName("name");
|
|
3059
|
+
if (byField?.text) return byField;
|
|
3060
|
+
return this.findChild(node, (n) => n.type === "identifier" || n.type === "constructor");
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* The name an `operator_definition` declares. Same normalization as
|
|
3064
|
+
* `declarationName`'s parenthesized branch, so a signature and its definition
|
|
3065
|
+
* land on ONE key and merge instead of producing two symbols.
|
|
3066
|
+
*/
|
|
3067
|
+
operatorDefinitionName(node) {
|
|
3068
|
+
const nameNode = node.childForFieldName("name") ?? this.findChild(node, (n) => n.type === "parenthesized_operator" || n.type === "parenthesized_name");
|
|
3069
|
+
if (!nameNode?.text) return null;
|
|
3070
|
+
return this.unparenthesize(nameNode.text);
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* The name a `type_signature` DECLARES, normalized across the four shapes the
|
|
3074
|
+
* pinned grammar puts inside `signature_name`:
|
|
3075
|
+
*
|
|
3076
|
+
* identifier `f :: !Int -> Bool`
|
|
3077
|
+
* constructor `NSMakeSize :: !NSSize !NSSizeType -> NSSize*`
|
|
3078
|
+
* — Clean lets an uppercase name be a FUNCTION and the
|
|
3079
|
+
* grammar types it `constructor`
|
|
3080
|
+
* parenthesized_operator `(%.) infixl 9 :: ...` — the name is `%.`
|
|
3081
|
+
* parenthesized_name `(bitxor) :: !Int !Int -> Int` — the name is
|
|
3082
|
+
* `bitxor` (a prefix declaration of an infix-capable
|
|
3083
|
+
* name)
|
|
3084
|
+
*
|
|
3085
|
+
* A parenthesized shape carrying a comma or a brace is NOT a name: it is the
|
|
3086
|
+
* grammar's mis-shape for a PATTERN (`# (a,b,c) = expr;`), and publishing it
|
|
3087
|
+
* would fabricate a symbol no source declares. Measured over the corpus
|
|
3088
|
+
* (312 files, 148 of them `.dcl`): 2,753 module-level signature sites, of which
|
|
3089
|
+
* 1,021 names were published nowhere — 708 identifiers, 259 constructors, 45
|
|
3090
|
+
* operators, 9 parenthesized names.
|
|
3091
|
+
*/
|
|
3092
|
+
declarationName(node) {
|
|
3093
|
+
const sigName = node.childForFieldName("name") ?? this.findChild(node, (n) => n.type === "signature_name");
|
|
3094
|
+
if (!sigName) return null;
|
|
3095
|
+
const kid = sigName.namedChildCount > 0 ? sigName.namedChild(0) : null;
|
|
3096
|
+
if (!kid) return null;
|
|
3097
|
+
if (kid.type === "identifier" || kid.type === "constructor") return kid.text;
|
|
3098
|
+
if (kid.type === "parenthesized_operator" || kid.type === "parenthesized_name") {
|
|
3099
|
+
return this.unparenthesize(kid.text);
|
|
3100
|
+
}
|
|
3101
|
+
return null;
|
|
3102
|
+
}
|
|
3103
|
+
unparenthesize(text) {
|
|
3104
|
+
return unparenthesizeText(text);
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Is `name` a class this file declares, under the OTHER spelling Clean uses for an
|
|
3108
|
+
* operator-named class?
|
|
3109
|
+
*
|
|
3110
|
+
* A declaration writes `class (==) …` and an instance reads `instance == …`, so
|
|
3111
|
+
* `declaredTypeNames` holds `(==)` while the lookup holds `==`. Only the parenthesised
|
|
3112
|
+
* direction is tried, and only when the bare name is not itself declared, so a file
|
|
3113
|
+
* that declares BOTH `class (===)` and `class ===` still resolves `===` to the
|
|
3114
|
+
* declaration that actually spells it that way. Returns false for any name that is not
|
|
3115
|
+
* a plausible operator, which keeps this from matching an ordinary identifier.
|
|
3116
|
+
*/
|
|
3117
|
+
isLocallyDeclaredOperatorClass(ctx, name) {
|
|
3118
|
+
if (this.isPlainIdentifierName(name)) return false;
|
|
3119
|
+
const parenthesised = `(${name})`;
|
|
3120
|
+
return ctx.declaredTypeNames.has(parenthesised) || ctx.declaredClassNames.has(parenthesised);
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Is `name` an ordinary identifier — a letter or `_`, then letters, digits, `_` or `'`?
|
|
3124
|
+
*
|
|
3125
|
+
* Character tests, not a pattern. This file's one regex-free property is checked by
|
|
3126
|
+
* `script/m14/clean-completeness-gate.ts`, which fails on ANY `RegularExpressionLiteral`
|
|
3127
|
+
* — a stronger rule than the M49.R2 "no regex doing syntax work" policy, and the reason a
|
|
3128
|
+
* two-line predicate like `/^[A-Za-z_][A-Za-z0-9_']*$/` had to be written this way. The
|
|
3129
|
+
* first version of `isLocallyDeclaredOperatorClass` used that regex and turned the gate
|
|
3130
|
+
* red, which is the gate working: it caught a regression introduced by the very commit
|
|
3131
|
+
* that added the fixture meant to prove the correction.
|
|
3132
|
+
*/
|
|
3133
|
+
isPlainIdentifierName(name) {
|
|
3134
|
+
if (name.length === 0) return false;
|
|
3135
|
+
const isHead = (c) => c === "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z";
|
|
3136
|
+
if (!isHead(name[0])) return false;
|
|
3137
|
+
for (let i = 1; i < name.length; i++) {
|
|
3138
|
+
const c = name[i];
|
|
3139
|
+
if (!isHead(c) && !(c >= "0" && c <= "9") && c !== "'") return false;
|
|
3140
|
+
}
|
|
3141
|
+
return true;
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* The comments belonging to a declaration: the run of comment nodes that
|
|
3145
|
+
* IMMEDIATELY precede it, plus any comment inside it.
|
|
3146
|
+
*
|
|
3147
|
+
* Contiguity is the boundary. Without it a trailing remark about the PREVIOUS
|
|
3148
|
+
* declaration — the common case in Clean's `.dcl` files, where every interface
|
|
3149
|
+
* member is introduced by a comment — is stolen by the next one, and a `.dcl`
|
|
3150
|
+
* that documents twenty members would give all twenty the same text. So the
|
|
3151
|
+
* walk stops at the first comment with a blank line between it and the
|
|
3152
|
+
* declaration, which is the same rule the TypeScript lane uses
|
|
3153
|
+
* (real-typescript-analyzer.ts:5439).
|
|
3154
|
+
*
|
|
3155
|
+
* 3,062 comment nodes over the corpus (2,679 line, 383 block) reached no symbol
|
|
3156
|
+
* before this: the lane published a symbol's type, parameters and generics and
|
|
3157
|
+
* never its documentation, so `brain_symbol.docstring` was empty for every Clean
|
|
3158
|
+
* symbol in existence.
|
|
3159
|
+
*/
|
|
3160
|
+
/**
|
|
3161
|
+
* Add a node's comments to a symbol that already has some.
|
|
3162
|
+
*
|
|
3163
|
+
* The order is signature-then-definition, which is the source order for the
|
|
3164
|
+
* `.dcl` house style (documentation above the signature, definition below), and
|
|
3165
|
+
* the DEFINITION's leading run takes the `docstring` when it has one.
|
|
3166
|
+
*/
|
|
3167
|
+
mergeComments(sym, node) {
|
|
3168
|
+
const before = sym.comments?.length ?? 0;
|
|
3169
|
+
this.attachComments(sym, node);
|
|
3170
|
+
if ((sym.comments?.length ?? 0) === before) return;
|
|
3171
|
+
if (this.leadingComments(node).length > 0 && !sym.docstring) sym.docstring = sym.comments?.[0];
|
|
3172
|
+
}
|
|
3173
|
+
attachComments(sym, node) {
|
|
3174
|
+
const leading = this.leadingComments(node);
|
|
3175
|
+
const inner = [];
|
|
3176
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3177
|
+
const child = node.namedChild(i);
|
|
3178
|
+
if (!child || !COMMENT_TYPES.has(child.type)) continue;
|
|
3179
|
+
const text = commentText(child.text);
|
|
3180
|
+
if (text && !leading.includes(text) && !inner.includes(text)) inner.push(text);
|
|
3181
|
+
}
|
|
3182
|
+
const trailing = this.trailingComments(node);
|
|
3183
|
+
const all = [...leading, ...trailing, ...inner];
|
|
3184
|
+
if (all.length === 0) return;
|
|
3185
|
+
const existing = sym.comments ?? [];
|
|
3186
|
+
const merged = [...existing];
|
|
3187
|
+
for (const c of all) if (!merged.includes(c)) merged.push(c);
|
|
3188
|
+
sym.comments = merged;
|
|
3189
|
+
if (leading.length > 0 && !sym.docstring) sym.docstring = leading[0];
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* The contiguous run of comment nodes IMMEDIATELY above a declaration, in
|
|
3193
|
+
* source order.
|
|
3194
|
+
*
|
|
3195
|
+
* Two boundaries, and the first implementation had only one:
|
|
3196
|
+
*
|
|
3197
|
+
* * a BLANK LINE ends the run — otherwise a remark about the PREVIOUS
|
|
3198
|
+
* declaration is stolen by the next one, and a `.dcl` that documents twenty
|
|
3199
|
+
* members would give all twenty the same text;
|
|
3200
|
+
* * a comment that begins ON the preceding declaration's own line is that
|
|
3201
|
+
* declaration's TRAILING comment, not this one's documentation. Clean's
|
|
3202
|
+
* grammar puts such a comment in the source-file child list right after the
|
|
3203
|
+
* declaration it annotates, so it is the next declaration's
|
|
3204
|
+
* `previousSibling` and the blank-line rule alone cannot exclude it. The
|
|
3205
|
+
* measured damage: `initAppDelegate` was documented with
|
|
3206
|
+
* "create the AppDelegate class", which is `createAppDelegate`'s comment.
|
|
3207
|
+
* The barrier is the nearest preceding NON-comment sibling's end row; a
|
|
3208
|
+
* comment starting at or above it belongs to that sibling.
|
|
3209
|
+
*/
|
|
3210
|
+
leadingComments(node) {
|
|
3211
|
+
const parent = node.parent;
|
|
3212
|
+
if (!parent) return [];
|
|
3213
|
+
const at = this.childIndexOf(parent, node);
|
|
3214
|
+
if (at < 0) return [];
|
|
3215
|
+
let barrierRow = -1;
|
|
3216
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
3217
|
+
const c = parent.child(i);
|
|
3218
|
+
if (!c || !c.isNamed) continue;
|
|
3219
|
+
if (COMMENT_TYPES.has(c.type)) continue;
|
|
3220
|
+
barrierRow = c.endPosition.row;
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
const out = [];
|
|
3224
|
+
let anchorRow = node.startPosition.row;
|
|
3225
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
3226
|
+
const c = parent.child(i);
|
|
3227
|
+
if (!c || !c.isNamed || !COMMENT_TYPES.has(c.type)) break;
|
|
3228
|
+
if (c.endPosition.row + 1 < anchorRow) break;
|
|
3229
|
+
if (c.startPosition.row <= barrierRow) break;
|
|
3230
|
+
const text = commentText(c.text);
|
|
3231
|
+
if (text) out.unshift(text);
|
|
3232
|
+
anchorRow = c.startPosition.row;
|
|
3233
|
+
}
|
|
3234
|
+
return out;
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* A TRAILING comment: the run of comment nodes that start on the declaration's
|
|
3238
|
+
* own END row.
|
|
3239
|
+
*
|
|
3240
|
+
* Clean's house style in every `.dcl` in this corpus is a remark at the end of
|
|
3241
|
+
* the signature line, and this grammar places it in the PARENT's child list
|
|
3242
|
+
* after the declaration — so it is also the next declaration's leading
|
|
3243
|
+
* neighbour, which is why the barrier in `leadingComments` exists.
|
|
3244
|
+
*/
|
|
3245
|
+
trailingComments(node) {
|
|
3246
|
+
const parent = node.parent;
|
|
3247
|
+
if (!parent) return [];
|
|
3248
|
+
const at = this.childIndexOf(parent, node);
|
|
3249
|
+
if (at < 0) return [];
|
|
3250
|
+
const out = [];
|
|
3251
|
+
for (let i = at + 1; i < parent.childCount; i++) {
|
|
3252
|
+
const c = parent.child(i);
|
|
3253
|
+
if (!c || !c.isNamed || !COMMENT_TYPES.has(c.type)) break;
|
|
3254
|
+
if (c.startPosition.row !== node.endPosition.row) break;
|
|
3255
|
+
const text = commentText(c.text);
|
|
3256
|
+
if (text && !out.includes(text)) out.push(text);
|
|
3257
|
+
}
|
|
3258
|
+
return out;
|
|
3259
|
+
}
|
|
3260
|
+
/**
|
|
3261
|
+
* This node's index in its parent's child list, or -1.
|
|
3262
|
+
*
|
|
3263
|
+
* It is found by START INDEX rather than by identity because web-tree-sitter
|
|
3264
|
+
* recreates a Node object on every access — and the sibling ACCESSORS cannot be
|
|
3265
|
+
* used at all on this grammar: measured, `previousSibling` here returns the
|
|
3266
|
+
* previous NAMED NON-COMMENT sibling, skipping the very comment nodes this
|
|
3267
|
+
* method exists to find, so `total`'s leading block comment was unreachable
|
|
3268
|
+
* through it. Scanning the child list is the construction that works, and it is
|
|
3269
|
+
* the same one the rest of this file uses to find a node's neighbours.
|
|
3270
|
+
*/
|
|
3271
|
+
childIndexOf(parent, child) {
|
|
3272
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
3273
|
+
const c = parent.child(i);
|
|
3274
|
+
if (c && c.startIndex === child.startIndex && c.endIndex === child.endIndex) return i;
|
|
3275
|
+
}
|
|
3276
|
+
return -1;
|
|
3277
|
+
}
|
|
3278
|
+
// ============================================================================
|
|
3279
|
+
// AST PLUMBING
|
|
3280
|
+
//
|
|
3281
|
+
// Everything here reads a node's own structure. No method inspects a node's
|
|
3282
|
+
// text to decide what it is: the grammar's FIELDS are the discriminator
|
|
3283
|
+
// everywhere, and the only text-level operations in the file are the four
|
|
3284
|
+
// positional ones already justified at their definitions (the unparenthesize
|
|
3285
|
+
// pair, the dot strip, the quote drop, the whitespace fold).
|
|
3286
|
+
// ============================================================================
|
|
3287
|
+
/**
|
|
3288
|
+
* `childrenForFieldName` with the null entries dropped.
|
|
3289
|
+
*
|
|
3290
|
+
* web-tree-sitter's `childrenForFieldName` is typed `(Node | null)[]` because
|
|
3291
|
+
* it walks the whole child list matching field ids. Every caller here wants the
|
|
3292
|
+
* real children, and a `for..of` over the raw array would need a non-null
|
|
3293
|
+
* assertion at each use. One filter, here, is both shorter and honest about the
|
|
3294
|
+
* shape.
|
|
3295
|
+
*/
|
|
3296
|
+
fields(node, name) {
|
|
3297
|
+
const raw = node.childrenForFieldName(name);
|
|
3298
|
+
const out = [];
|
|
3299
|
+
for (let i = 0; i < raw.length; i++) {
|
|
3300
|
+
const c = raw[i];
|
|
3301
|
+
if (c) out.push(c);
|
|
3302
|
+
}
|
|
3303
|
+
return out;
|
|
3304
|
+
}
|
|
3305
|
+
findChild(node, predicate) {
|
|
3306
|
+
const count = node.namedChildCount;
|
|
3307
|
+
for (let i = 0; i < count; i++) {
|
|
3308
|
+
const child = node.namedChild(i);
|
|
3309
|
+
if (child && predicate(child)) return child;
|
|
3310
|
+
}
|
|
3311
|
+
return null;
|
|
3312
|
+
}
|
|
3313
|
+
filterChildren(node, predicate) {
|
|
3314
|
+
const result = [];
|
|
3315
|
+
const count = node.namedChildCount;
|
|
3316
|
+
for (let i = 0; i < count; i++) {
|
|
3317
|
+
const child = node.namedChild(i);
|
|
3318
|
+
if (child && predicate(child)) result.push(child);
|
|
3319
|
+
}
|
|
3320
|
+
return result;
|
|
3321
|
+
}
|
|
3322
|
+
firstNamedChildOfType(node, type) {
|
|
3323
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3324
|
+
const c = node.namedChild(i);
|
|
3325
|
+
if (c && c.type === type) return c;
|
|
3326
|
+
}
|
|
3327
|
+
return null;
|
|
3328
|
+
}
|
|
3329
|
+
/** A child the grammar leaves UNFIELDED — `generic_declaration`'s `::` type. */
|
|
3330
|
+
unnamedChildOfType(node, type) {
|
|
3331
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3332
|
+
const c = node.child(i);
|
|
3333
|
+
if (c && c.type === type) return c;
|
|
3334
|
+
}
|
|
3335
|
+
return null;
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* A name node normalized for a parenthesized operator or name: `(==)` → `==`,
|
|
3339
|
+
* anything else → its own text.
|
|
3340
|
+
*
|
|
3341
|
+
* The parenthesized forms are how Clean spells an infix-capable name, and the
|
|
3342
|
+
* four shapes that carry a name (`constructor`, `identifier`,
|
|
3343
|
+
* `parenthesized_operator`, `parenthesized_name`) must all land on ONE key, or
|
|
3344
|
+
* a signature and its definition become two symbols.
|
|
3345
|
+
*/
|
|
3346
|
+
normalizeParenthesized(node) {
|
|
3347
|
+
if (!node?.text) return null;
|
|
3348
|
+
if (node.type === "parenthesized_operator" || node.type === "parenthesized_name") {
|
|
3349
|
+
return this.unparenthesize(node.text);
|
|
3350
|
+
}
|
|
3351
|
+
return node.text;
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* The name a `class_member` / `instance_member` wrapper's inner declaration
|
|
3355
|
+
* declares, normalized across all three shapes the two wrappers carry.
|
|
3356
|
+
*
|
|
3357
|
+
* A `type_signature` wraps its name in a `signature_name` (four shapes, see
|
|
3358
|
+
* `declarationName`); an `operator_definition` puts a `parenthesized_operator`
|
|
3359
|
+
* straight in the `name` field; a `function_declaration` puts an `identifier`.
|
|
3360
|
+
* All three are read from the FIELD, never from the first child, for the reason
|
|
3361
|
+
* `declaredName` documents: an uppercase definition's name is typed
|
|
3362
|
+
* `constructor` and a first-child scan publishes the first PARAMETER instead.
|
|
3363
|
+
*/
|
|
3364
|
+
memberDeclaredName(wrapper) {
|
|
3365
|
+
const inner = wrapper.namedChild(0);
|
|
3366
|
+
if (!inner) return null;
|
|
3367
|
+
if (inner.type === "type_signature") return this.declarationName(inner);
|
|
3368
|
+
return this.normalizeParenthesized(this.declaredName(inner));
|
|
3369
|
+
}
|
|
3370
|
+
/** module_name children compose dotted names (Data.Map → Data + "." + Map). */
|
|
3371
|
+
assembleModuleName(node) {
|
|
3372
|
+
let out = "";
|
|
3373
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3374
|
+
const c = node.namedChild(i);
|
|
3375
|
+
if (!c) continue;
|
|
3376
|
+
const leaf = c.type === "module_name" ? this.assembleModuleName(c) : c.text;
|
|
3377
|
+
if (out) out += ".";
|
|
3378
|
+
out += leaf;
|
|
3379
|
+
}
|
|
3380
|
+
return out || node.text;
|
|
3381
|
+
}
|
|
3382
|
+
nodeToRange(node) {
|
|
3383
|
+
return {
|
|
3384
|
+
startLine: node.startPosition.row + 1,
|
|
3385
|
+
startColumn: node.startPosition.column + 1,
|
|
3386
|
+
endLine: node.endPosition.row + 1,
|
|
3387
|
+
endColumn: node.endPosition.column + 1,
|
|
3388
|
+
startIndex: node.startIndex,
|
|
3389
|
+
endIndex: node.endIndex
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Is this application in PATTERN territory?
|
|
3394
|
+
*
|
|
3395
|
+
* A constructor application is an `instantiates` in a body and a match in a
|
|
3396
|
+
* pattern, and the two are different edges. Pattern territory is:
|
|
3397
|
+
* * any pattern-node subtree (`constructor_pattern`, `list_pattern`,
|
|
3398
|
+
* `tuple_pattern`, `record_pattern`, `paren_pattern`, `unboxed_pattern`,
|
|
3399
|
+
* `cons_pattern`, `strict_binding_pattern`, `record_update_pattern`);
|
|
3400
|
+
* * a `case_alternative` head — the pattern before the `->`;
|
|
3401
|
+
* * everything in a definition BEFORE its `body` FIELD (the head patterns).
|
|
3402
|
+
*
|
|
3403
|
+
* The pre-#884 form of the third rule scanned the children for one typed
|
|
3404
|
+
* `operator` with text `=`, which the pinned grammar cannot produce: a
|
|
3405
|
+
* definition's s-expression carries name/pattern…/body and NO `=` child at all
|
|
3406
|
+
* (measured three ways — the 1,576 `operator` nodes in the corpus have parents
|
|
3407
|
+
* `binary_expression` 829, `unary_expression` 99, `paren_pattern` 9,
|
|
3408
|
+
* `operator_definition` 5, `instance_class` 11 and never a
|
|
3409
|
+
* `function_declaration`, and no `operator` node's text is `=`). Expressed
|
|
3410
|
+
* against the field, the rule is live and means the same thing.
|
|
3411
|
+
*/
|
|
3412
|
+
isPatternPosition(node) {
|
|
3413
|
+
let cur = node.parent;
|
|
3414
|
+
while (cur) {
|
|
3415
|
+
if (PATTERN_NODE_TYPES.has(cur.type)) return true;
|
|
3416
|
+
if (cur.type === "case_alternative") {
|
|
3417
|
+
const first = cur.namedChild(0);
|
|
3418
|
+
return first !== null && first.startIndex === node.startIndex || this.isBeforeArrow(cur, node);
|
|
3419
|
+
}
|
|
3420
|
+
if (CALLABLE_NODE_TYPES.has(cur.type)) return this.isBeforeBody(cur, node);
|
|
3421
|
+
if (cur.type === "type_definition" || cur.type === "class_declaration" || cur.type === "instance_declaration" || cur.type === "type_signature") {
|
|
3422
|
+
return false;
|
|
3423
|
+
}
|
|
3424
|
+
cur = cur.parent;
|
|
3425
|
+
}
|
|
3426
|
+
return false;
|
|
3427
|
+
}
|
|
3428
|
+
isBeforeArrow(caseAlt, node) {
|
|
3429
|
+
for (let i = 0; i < caseAlt.namedChildCount; i++) {
|
|
3430
|
+
const c = caseAlt.namedChild(i);
|
|
3431
|
+
if (!c) continue;
|
|
3432
|
+
if (c.type === "arrow") return node.startIndex < c.startIndex;
|
|
3433
|
+
if (c.startIndex === node.startIndex) return true;
|
|
3434
|
+
}
|
|
3435
|
+
return false;
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* Pattern territory inside a definition is everything BEFORE its body FIELD. A
|
|
3439
|
+
* definition with no body field at all (a damaged parse) still has `pattern`
|
|
3440
|
+
* fields, and territory is everything before the LAST of them, so a constructor
|
|
3441
|
+
* application in a body-less node is not mistaken for a match.
|
|
3442
|
+
*/
|
|
3443
|
+
isBeforeBody(defNode, node) {
|
|
3444
|
+
for (const b of this.fields(defNode, "body")) {
|
|
3445
|
+
if (node.startIndex < b.startIndex) return true;
|
|
3446
|
+
}
|
|
3447
|
+
const patterns = this.fields(defNode, "pattern");
|
|
3448
|
+
if (patterns.length === 0) return false;
|
|
3449
|
+
let last = -1;
|
|
3450
|
+
for (const p of patterns) last = Math.max(last, p.endIndex);
|
|
3451
|
+
return node.startIndex < last;
|
|
3452
|
+
}
|
|
3453
|
+
};
|
|
3454
|
+
function commentText(raw) {
|
|
3455
|
+
let s = raw;
|
|
3456
|
+
if (s.startsWith("/*")) {
|
|
3457
|
+
s = s.slice(2);
|
|
3458
|
+
while (s.startsWith("*")) s = s.slice(1);
|
|
3459
|
+
if (s.endsWith("*/")) s = s.slice(0, -2);
|
|
3460
|
+
return s.trim();
|
|
3461
|
+
}
|
|
3462
|
+
while (s.startsWith("/")) s = s.slice(1);
|
|
3463
|
+
return s.trim();
|
|
3464
|
+
}
|
|
3465
|
+
function stripLeadingDot(text) {
|
|
3466
|
+
return text.startsWith(".") ? text.slice(1) : text;
|
|
3467
|
+
}
|
|
3468
|
+
export {
|
|
3469
|
+
RealCleanAnalyzer
|
|
3470
|
+
};
|