@suss/resolution 0.27.0 → 0.29.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/dist/index.js CHANGED
@@ -1,3 +1,263 @@
1
+ // src/program.ts
2
+ import { clearRelations, deriveOnDemand, evaluate } from "@suss/datalog";
3
+ var ASKING_RELATIONS = [
4
+ "wanted",
5
+ "wantedOrigin",
6
+ "wantedCallOrigin",
7
+ "wantedExportsOf",
8
+ "wantedSubject",
9
+ "wantedAnchor",
10
+ "wantedAncestry"
11
+ ];
12
+ var NO_LANGUAGE_RULES = [];
13
+ var programs = /* @__PURE__ */ new WeakMap();
14
+ function resolutionProgram(languageRules = NO_LANGUAGE_RULES) {
15
+ const built = programs.get(languageRules);
16
+ if (built !== void 0) {
17
+ return built;
18
+ }
19
+ const rules = [
20
+ ...RESOLUTION_RULES,
21
+ ...languageRules,
22
+ ...RESOLUTION_QUESTIONS
23
+ ];
24
+ const program = process.env.SUSS_RESOLUTION_ON_DEMAND === "0" ? { rules, demandDriven: [], demands: [] } : deriveOnDemand(rules, ANSWER_RELATIONS);
25
+ programs.set(languageRules, program);
26
+ return program;
27
+ }
28
+ function queryFacts(program) {
29
+ if (program.demandDriven.length === 0) {
30
+ return [];
31
+ }
32
+ return [...program.demandDriven, ...ASKING_RELATIONS];
33
+ }
34
+ var askedByDb = /* @__PURE__ */ new WeakMap();
35
+ function askedOf(db, asking) {
36
+ let byRelation = askedByDb.get(db);
37
+ if (byRelation === void 0) {
38
+ byRelation = /* @__PURE__ */ new Map();
39
+ askedByDb.set(db, byRelation);
40
+ }
41
+ let asked = byRelation.get(asking);
42
+ if (asked === void 0) {
43
+ asked = /* @__PURE__ */ new Set();
44
+ byRelation.set(asking, asked);
45
+ }
46
+ return asked;
47
+ }
48
+ function askResolution(db, keys, asking = "wanted", program = resolutionProgram()) {
49
+ const asked = askedOf(db, asking);
50
+ const fresh = [...keys].filter((key) => !asked.has(key));
51
+ if (fresh.length === 0) {
52
+ return;
53
+ }
54
+ for (const key of fresh) {
55
+ asked.add(key);
56
+ db.add(asking, [key]);
57
+ }
58
+ evaluate(db, program.rules);
59
+ const forget = queryFacts(program);
60
+ if (forget.length > 0) {
61
+ clearRelations(db, program.rules, forget);
62
+ }
63
+ }
64
+
65
+ // src/callee.ts
66
+ var UNDECLARED = { kind: "undeclared" };
67
+ var UNSETTLED = { kind: "unsettled" };
68
+ var OUTSIDE_RUN = { kind: "outsideRun" };
69
+ function calleeOutcomes(db, keys, program = resolutionProgram()) {
70
+ askAboutSources(db, keys, program);
71
+ const reading = new Reading(db);
72
+ const outcomes = /* @__PURE__ */ new Map();
73
+ for (const key of keys) {
74
+ outcomes.set(key, reading.outcomeOf(key));
75
+ }
76
+ return outcomes;
77
+ }
78
+ function calleeOutcomeOf(db, key, program = resolutionProgram()) {
79
+ return calleeOutcomes(db, [key], program).get(key) ?? UNDECLARED;
80
+ }
81
+ function askAboutSources(db, keys, program) {
82
+ let wave = [...new Set(keys)];
83
+ const asked = new Set(wave);
84
+ while (wave.length > 0) {
85
+ askResolution(db, wave, "wanted", program);
86
+ const next = [];
87
+ for (const key of wave) {
88
+ if (!decidedByCallers(db, key) && answersFor(db, key).length > 0) {
89
+ continue;
90
+ }
91
+ for (const source of sourcesOf(db, key)) {
92
+ if (!asked.has(source)) {
93
+ asked.add(source);
94
+ next.push(source);
95
+ }
96
+ }
97
+ }
98
+ wave = next;
99
+ }
100
+ }
101
+ function couldBeSettled(db, key) {
102
+ return db.has("func", [key]) || db.has("objectValue", [key]) || sourcesOf(db, key).length > 0;
103
+ }
104
+ function sourcesOf(db, key) {
105
+ const base = db.lookup("readsProperty", 0, key)[0];
106
+ if (base !== void 0) {
107
+ return [String(base[1])];
108
+ }
109
+ const call = db.lookup("call", 0, key)[0];
110
+ if (call !== void 0) {
111
+ return [String(call[1])];
112
+ }
113
+ return writtenSourcesOf(db, key);
114
+ }
115
+ var WRITE_RELATIONS = ["binds", "endsHolding", "mayHold"];
116
+ function writtenSourcesOf(db, key) {
117
+ const found = [];
118
+ for (const relation of WRITE_RELATIONS) {
119
+ for (const row of db.lookup(relation, 0, key)) {
120
+ found.push(String(row[1]));
121
+ }
122
+ }
123
+ return found;
124
+ }
125
+ function answersFor(db, key) {
126
+ const found = /* @__PURE__ */ new Map();
127
+ for (const row of db.lookup("wantedComesTo", 0, key)) {
128
+ const answer = String(row[1]);
129
+ if (db.has("func", [answer])) {
130
+ found.set(answer, { kind: "function", key: answer });
131
+ continue;
132
+ }
133
+ if (db.has("objectValue", [answer])) {
134
+ found.set(answer, { kind: "object", key: answer });
135
+ }
136
+ }
137
+ return [...found.values()];
138
+ }
139
+ function outcomeKey(outcome) {
140
+ if (outcome.kind === "function" || outcome.kind === "object") {
141
+ return `${outcome.kind}:${outcome.key}`;
142
+ }
143
+ return outcome.kind;
144
+ }
145
+ var THROUGH_BASE = {
146
+ function: UNDECLARED,
147
+ object: UNDECLARED
148
+ };
149
+ var THROUGH_CALL = {
150
+ function: UNSETTLED,
151
+ object: UNSETTLED
152
+ };
153
+ var Reading = class {
154
+ constructor(db) {
155
+ this.db = db;
156
+ }
157
+ db;
158
+ memo = /* @__PURE__ */ new Map();
159
+ walking = /* @__PURE__ */ new Set();
160
+ outcomeOf(key) {
161
+ return this.walk(key).outcome;
162
+ }
163
+ walk(key) {
164
+ const known = this.memo.get(key);
165
+ if (known !== void 0) {
166
+ return { outcome: known, looped: false };
167
+ }
168
+ if (this.walking.has(key)) {
169
+ return { outcome: UNDECLARED, looped: true };
170
+ }
171
+ this.walking.add(key);
172
+ const found = this.decide(key);
173
+ this.walking.delete(key);
174
+ if (!found.looped) {
175
+ this.memo.set(key, found.outcome);
176
+ }
177
+ return found;
178
+ }
179
+ decide(key) {
180
+ if (decidedByCallers(this.db, key)) {
181
+ return this.fromWrites(key);
182
+ }
183
+ const answers = answersFor(this.db, key);
184
+ if (answers.length > 1) {
185
+ return {
186
+ outcome: { kind: "severalSources", sources: answers.map(outcomeKey) },
187
+ looped: false
188
+ };
189
+ }
190
+ const only = answers[0];
191
+ if (only !== void 0) {
192
+ return { outcome: only, looped: false };
193
+ }
194
+ return this.refusal(key);
195
+ }
196
+ refusal(key) {
197
+ const base = this.db.lookup("readsProperty", 0, key)[0];
198
+ if (base !== void 0) {
199
+ return this.through(THROUGH_BASE, String(base[1]));
200
+ }
201
+ const call = this.db.lookup("call", 0, key)[0];
202
+ if (call !== void 0) {
203
+ return this.through(THROUGH_CALL, String(call[1]));
204
+ }
205
+ return this.fromWrites(key);
206
+ }
207
+ through(table, source) {
208
+ const found = this.walk(source);
209
+ return {
210
+ outcome: table[found.outcome.kind] ?? found.outcome,
211
+ looped: found.looped
212
+ };
213
+ }
214
+ /**
215
+ * What the writes to a name, and the arguments a call put in a
216
+ * parameter, agree the value is. A source that says nothing is left
217
+ * out, so a name narrowed through a second name keeps the outcome it
218
+ * started with rather than counting as a second source.
219
+ */
220
+ fromWrites(key) {
221
+ const found = /* @__PURE__ */ new Map();
222
+ let looped = false;
223
+ for (const source of writtenSourcesOf(this.db, key)) {
224
+ const from = this.walk(source);
225
+ looped = looped || from.looped;
226
+ if (from.outcome.kind !== "undeclared") {
227
+ found.set(outcomeKey(from.outcome), from.outcome);
228
+ }
229
+ }
230
+ if (isParameter(this.db, key)) {
231
+ found.set("callerSupplied", { kind: "callerSupplied", key });
232
+ }
233
+ if (found.size > 1) {
234
+ return {
235
+ outcome: { kind: "severalSources", sources: [...found.keys()] },
236
+ looped
237
+ };
238
+ }
239
+ const only = [...found.values()][0];
240
+ return { outcome: only ?? whereItCameFrom(this.db, key), looped };
241
+ }
242
+ };
243
+ function isParameter(db, key) {
244
+ return db.lookup("paramNamed", 2, key).length > 0 || db.lookup("paramOf", 2, key).length > 0;
245
+ }
246
+ function decidedByCallers(db, key) {
247
+ return isParameter(db, key) && db.lookup("binds", 0, key).length === 0;
248
+ }
249
+ function whereItCameFrom(db, key) {
250
+ for (const row of db.lookup("imports", 0, key)) {
251
+ if (db.lookup("exportsAs", 0, String(row[1])).length === 0) {
252
+ return OUTSIDE_RUN;
253
+ }
254
+ }
255
+ if (db.has("writtenValue", [key])) {
256
+ return OUTSIDE_RUN;
257
+ }
258
+ return db.has("writesUnstated", [key]) ? UNSETTLED : UNDECLARED;
259
+ }
260
+
1
261
  // src/contract.ts
2
262
  var distinct = (values) => new Set(values).size;
3
263
  var RELATIONS_THAT_CAN_CROSS = ["imports", "binds", "reExports"];
@@ -273,6 +533,38 @@ var DEFAULT_PHRASES = {
273
533
  ]
274
534
  };
275
535
  },
536
+ "declared finder": ({ tuple, premises, describe }) => {
537
+ const method = String(premises[1].tuple[2]);
538
+ const base = String(premises[4].tuple[0]);
539
+ return {
540
+ reason: `${describe(tuple[0])} calls ${method} on ${describe(tuple[1])}, which gives back one of it`,
541
+ assumptions: [
542
+ `a pack declares that ${method} on a class extending ${base} gives back one of that class`
543
+ ]
544
+ };
545
+ },
546
+ "declared argument finder": ({ tuple, premises, describe }) => {
547
+ const method = String(premises[1].tuple[2]);
548
+ const position = String(premises[2].tuple[1]);
549
+ const base = String(premises[3].tuple[0]);
550
+ return {
551
+ reason: `${describe(tuple[0])} calls ${method} with ${describe(tuple[1])} at argument ${position}, which gives back one of it`,
552
+ assumptions: [
553
+ `a pack declares that ${method} with a class extending ${base} at argument ${position} gives back one of that class`
554
+ ]
555
+ };
556
+ },
557
+ "declared import finder": ({ tuple, premises, describe }) => {
558
+ const module = String(premises[1].tuple[1]);
559
+ const name = String(premises[1].tuple[2]);
560
+ const position = String(premises[2].tuple[2]);
561
+ return {
562
+ reason: `${describe(tuple[0])} calls ${name} from ${module} with ${describe(tuple[1])} at argument ${position}, which gives back one of it`,
563
+ assumptions: [
564
+ `a pack declares that ${name} from ${module} gives back one of the class at argument ${position}`
565
+ ]
566
+ };
567
+ },
276
568
  "call result": ({ tuple, premises, describe, inline }) => {
277
569
  const invokes = premises[0];
278
570
  const invoked = invokes.tuple[1];
@@ -477,6 +769,47 @@ function pathsOf(edges, memo, childId, visiting) {
477
769
  return result;
478
770
  }
479
771
 
772
+ // src/nameReads.ts
773
+ function writesRunInOrder(scope, name, writes, rules) {
774
+ const last = writes[writes.length - 1];
775
+ if (scope === null || last === void 0) {
776
+ return false;
777
+ }
778
+ if (!writes.every((write) => write.direct)) {
779
+ return false;
780
+ }
781
+ return !isReadBefore(scope, name, last.at.startIndex, rules);
782
+ }
783
+ function isReadBefore(scope, name, position, rules) {
784
+ let found = false;
785
+ const visit = (node) => {
786
+ if (found || node.startIndex >= position) {
787
+ return;
788
+ }
789
+ if (rules.laterBodies.has(node.type)) {
790
+ return;
791
+ }
792
+ if (rules.nameTypes.has(node.type) && node.text === name) {
793
+ found = rules.isRead(node);
794
+ return;
795
+ }
796
+ for (const child of rules.childrenOf(node)) {
797
+ visit(child);
798
+ }
799
+ };
800
+ for (const child of rules.childrenOf(scope)) {
801
+ visit(child);
802
+ }
803
+ return found;
804
+ }
805
+ function startsAtName(node, name, rules) {
806
+ if (rules.nameTypes.has(node.type)) {
807
+ return node.text === name;
808
+ }
809
+ const inner = rules.readFirst(node);
810
+ return inner !== null && startsAtName(inner, name, rules);
811
+ }
812
+
480
813
  // src/nodeKey.ts
481
814
  var SPAN_KEY = /^(.*):(\d+)-(\d+)$/;
482
815
  function nodeOfKey(rootsByFile, key) {
@@ -504,7 +837,7 @@ function nodeOfKey(rootsByFile, key) {
504
837
  }
505
838
 
506
839
  // src/session.ts
507
- import { Database, evaluate, proofOf, witnesses } from "@suss/datalog";
840
+ import { Database, evaluate as evaluate2, proofOf, witnesses } from "@suss/datalog";
508
841
  function explainResolvedKey(options) {
509
842
  const { db, rules, key, locate, displayPath } = options;
510
843
  const proofDb = new Database();
@@ -516,7 +849,7 @@ function explainResolvedKey(options) {
516
849
  }
517
850
  }
518
851
  const started = performance.now();
519
- evaluate(proofDb, rules, witnesses);
852
+ evaluate2(proofDb, rules, witnesses);
520
853
  const evaluateMs = performance.now() - started;
521
854
  const derivedFacts = proofDb.relationNames().reduce((count, relation) => count + proofDb.size(relation), 0) - baseFacts;
522
855
  const targets = new Set(
@@ -590,6 +923,35 @@ function withoutPlaceholders(answers, placeholders) {
590
923
  return kept;
591
924
  }
592
925
 
926
+ // src/writes.ts
927
+ function valueLeftByWrites(writes, ordered) {
928
+ const deciding = writes.filter((write) => !write.narrowsName);
929
+ const left = deciding.length === 0 ? writes : deciding;
930
+ if (left.some((write) => write.value === null)) {
931
+ return null;
932
+ }
933
+ if (ordered) {
934
+ return left[left.length - 1]?.value ?? null;
935
+ }
936
+ return sharedConstruction(left);
937
+ }
938
+ function sharedConstruction(writes) {
939
+ let settled = null;
940
+ for (const write of writes) {
941
+ if (write.placeholder) {
942
+ continue;
943
+ }
944
+ if (write.construction === null) {
945
+ return null;
946
+ }
947
+ if (settled !== null && write.construction !== settled.construction) {
948
+ return null;
949
+ }
950
+ settled = write;
951
+ }
952
+ return settled?.value ?? null;
953
+ }
954
+
593
955
  // src/writtenValue.ts
594
956
  function writtenValueOf(db, key, ask) {
595
957
  const placeholders = placeholderValues(db);
@@ -689,6 +1051,57 @@ var RESOLUTION_RULES = [
689
1051
  ],
690
1052
  "class instance"
691
1053
  ),
1054
+ // A finder the library declares, keyed on the base a pack named so a
1055
+ // project class with a method of the same name on another hierarchy is
1056
+ // left alone. The DESIGN says what a chain of them composes into.
1057
+ rule(
1058
+ "stepsTo",
1059
+ [v("r"), v("cls"), VALUE_STEP],
1060
+ [
1061
+ lit("call", v("r"), v("c")),
1062
+ lit("readsProperty", v("c"), v("o"), v("m")),
1063
+ lit("objectOf", v("o"), v("cls")),
1064
+ lit("objectValue", v("cls")),
1065
+ // Before `libraryBase`, so the demand rewrite asks it with the base
1066
+ // bound and walks an ancestry only for a method a pack declared.
1067
+ lit("givesBackOne", v("n"), v("m")),
1068
+ lit("libraryBase", v("cls"), v("n"))
1069
+ ],
1070
+ "declared finder"
1071
+ ),
1072
+ // The same word, for a library that takes the class as an argument
1073
+ // instead of as the receiver. The argument having to reach the base a
1074
+ // pack named is what keeps an unrelated `get` out.
1075
+ rule(
1076
+ "stepsTo",
1077
+ [v("r"), v("cls"), VALUE_STEP],
1078
+ [
1079
+ lit("call", v("r"), v("c")),
1080
+ lit("readsProperty", v("c"), v("o"), v("m")),
1081
+ lit("callArg", v("r"), v("k"), v("a")),
1082
+ // Before `libraryBase`, so the demand rewrite asks it with the base
1083
+ // bound and walks an ancestry only for a method a pack declared.
1084
+ lit("givesBackOneOfArgument", v("n"), v("m"), v("k")),
1085
+ lit("objectOf", v("a"), v("cls")),
1086
+ lit("libraryBase", v("cls"), v("n"))
1087
+ ],
1088
+ "declared argument finder"
1089
+ ),
1090
+ // The same again for a function called on its own rather than read off
1091
+ // anything, keyed on the module it was imported from so a project
1092
+ // function spelled the same way is not mistaken for it.
1093
+ rule(
1094
+ "stepsTo",
1095
+ [v("r"), v("cls"), VALUE_STEP],
1096
+ [
1097
+ lit("call", v("r"), v("c")),
1098
+ lit("comesFrom", v("c"), v("mod"), v("n")),
1099
+ lit("givesBackOneOfImport", v("mod"), v("n"), v("k")),
1100
+ lit("callArg", v("r"), v("k"), v("a")),
1101
+ lit("objectOf", v("a"), v("cls"))
1102
+ ],
1103
+ "declared import finder"
1104
+ ),
692
1105
  // Wrapper transparency, derived: calling a factory that returns a
693
1106
  // function which calls its parameter k steps to argument k.
694
1107
  rule(
@@ -962,6 +1375,32 @@ var RESOLUTION_RULES = [
962
1375
  lit("contains", v("baseCls"), v("n"), v("held"))
963
1376
  ]
964
1377
  ),
1378
+ // The library base a class's ancestry arrives at, however many of a
1379
+ // project's own classes sit in between. A base the run declares has a
1380
+ // node to keep walking from; one it does not has only a name.
1381
+ rule("libraryBase", [v("c"), v("n")], [lit("extendsNamed", v("c"), v("n"))]),
1382
+ rule(
1383
+ "libraryBase",
1384
+ [v("c"), v("n")],
1385
+ [
1386
+ lit("extends", v("c"), v("x")),
1387
+ lit("comesTo", v("x"), v("b")),
1388
+ lit("libraryBase", v("b"), v("n"))
1389
+ ]
1390
+ ),
1391
+ // A base a library hands back from a call rather than exporting as a
1392
+ // class, SQLAlchemy's `Base = declarative_base()`. The walk stops at
1393
+ // the call, so the function it called is the name to match on.
1394
+ rule(
1395
+ "libraryBase",
1396
+ [v("c"), v("n")],
1397
+ [
1398
+ lit("extends", v("c"), v("x")),
1399
+ lit("isWrittenAs", v("x"), v("r")),
1400
+ lit("call", v("r"), v("cc")),
1401
+ lit("comesFrom", v("cc"), v("m"), v("n"))
1402
+ ]
1403
+ ),
965
1404
  // Which function a factory returns, and which of its parameters that
966
1405
  // function calls: together they are what makes it a wrapper.
967
1406
  rule(
@@ -1310,6 +1749,17 @@ var RESOLUTION_QUESTIONS = [
1310
1749
  [v("c"), v("n")],
1311
1750
  [lit("ancestryChain", v("c"), v("b")), lit("extendsNamed", v("b"), v("n"))]
1312
1751
  ),
1752
+ // Each method the same chain declares, so a caller that settled a
1753
+ // receiver on a class can tell a method the project writes itself
1754
+ // from one only the library provides.
1755
+ rule(
1756
+ "wantedDeclaredName",
1757
+ [v("c"), v("n")],
1758
+ [
1759
+ lit("ancestryChain", v("c"), v("b")),
1760
+ lit("holdsProperty", v("b"), v("n"), v("held"))
1761
+ ]
1762
+ ),
1313
1763
  rule("anchorChain", [v("x"), v("x")], [lit("wantedAnchor", v("x"))]),
1314
1764
  rule(
1315
1765
  "anchorChain",
@@ -1359,6 +1809,7 @@ var ANSWER_RELATIONS = [
1359
1809
  ];
1360
1810
  export {
1361
1811
  ANSWER_RELATIONS,
1812
+ ASKING_RELATIONS,
1362
1813
  FACT_CONTRACT_CASES,
1363
1814
  NAMESPACE_IMPORT,
1364
1815
  NAMESPACE_IMPORT_NAME,
@@ -1367,14 +1818,24 @@ export {
1367
1818
  RESULT_STEP,
1368
1819
  VALUE_STEP,
1369
1820
  agreedMountPrefix,
1821
+ askResolution,
1822
+ calleeOutcomeOf,
1823
+ calleeOutcomes,
1370
1824
  checkFactContract,
1825
+ couldBeSettled,
1371
1826
  explainResolutionProof,
1372
1827
  explainResolvedKey,
1373
1828
  joinMountedPath,
1374
1829
  mountPathsOf,
1375
1830
  nodeOfKey,
1376
1831
  placeholderValues,
1832
+ queryFacts,
1377
1833
  renderExplanation,
1834
+ resolutionProgram,
1378
1835
  singleAnswers,
1836
+ startsAtName,
1837
+ valueLeftByWrites,
1838
+ writesRunInOrder,
1839
+ writtenSourcesOf,
1379
1840
  writtenValueOf
1380
1841
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/resolution",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "description": "The rules suss uses to follow a name in TypeScript, Python or Ruby to the function it comes down to.",
6
6
  "keywords": [
@@ -56,6 +56,6 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@suss/datalog": "0.27.0"
59
+ "@suss/datalog": "0.29.0"
60
60
  }
61
61
  }