@suss/resolution 0.3.2 → 0.5.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/README.md CHANGED
@@ -1,19 +1,20 @@
1
1
  # @suss/resolution
2
2
 
3
- The rules for following a value to the function it comes down to.
3
+ The rules for following a value to the function it ends up being.
4
4
 
5
5
  ## What this package is
6
6
 
7
- A list of Datalog rules and nothing else. No parser, no language, no
8
- files. An adapter reads source into facts, concatenates its own rules
9
- onto these, and evaluates on `@suss/datalog`.
7
+ A list of Datalog rules, and nothing else. It has no parser, no
8
+ language, and no files of its own. An adapter reads source into facts,
9
+ adds its own rules onto these, and evaluates the whole set on
10
+ `@suss/datalog`.
10
11
 
11
- The rules are about programming languages rather than about any one of
12
- them. A name binds to a value. A call puts an argument in a parameter. A
13
- module exports a name and another module can forward it. A function that
14
- returns a function calling its parameter hands back the argument it was
15
- given, which is a decorator in Python, a closure in Go, and a handler
16
- factory in TypeScript.
12
+ The rules are about programming languages in general rather than about
13
+ any one of them. A name binds to a value. A call puts an argument in a
14
+ parameter. A module exports a name, and another module can forward it.
15
+ A function that returns a function calling its parameter hands back the
16
+ argument it was given, which is a decorator in Python, a closure in Go,
17
+ and a handler factory in TypeScript.
17
18
 
18
19
  ## The facts an adapter supplies
19
20
 
@@ -28,6 +29,8 @@ binds(x, y) the name x is declared as y
28
29
  endsHolding(x, y) the name x is written more than once and
29
30
  holds y once the writes have run
30
31
  paramOf(f, k, p) p is f's parameter at position k
32
+ paramNamed(f, n, p) p is f's parameter called n
33
+ extends(c, b) class c is written as extending b
31
34
  returnsValue(f, v) f returns v
32
35
  bodyCalls(f, c) f's body calls c
33
36
  containsFn(f, g) g is declared inside f
@@ -41,9 +44,16 @@ reExportsAll(m, m2) m forwards everything m2 exports
41
44
 
42
45
  Two more come from a pack rather than from source, for a wrapper whose
43
46
  body nobody can read: `unwrapsByName(name, k)` and
44
- `wrapperModule(name, module)`, checked against `calleeName` and
45
- `calleeOrigin` so a local function spelled like the library's does not
46
- match.
47
+ `wrapperModule(name, module)`. Both are checked against `calleeName` and
48
+ `calleeOrigin`, so a local function that happens to be spelled the same
49
+ as the library's does not match.
50
+
51
+ `holdsProperty` is something an adapter states and the rules only read.
52
+ What a value contains, a base class included, comes out as `contains`,
53
+ so a method a base declares is found on a subclass that never overrode
54
+ it. Deriving that into `holdsProperty` instead turns it from a fact into
55
+ a derived relation, and the on-demand rewrite then gates it behind a
56
+ demand nothing generates.
47
57
 
48
58
  Node identity is the adapter's business. The rules only join on it.
49
59
 
@@ -58,87 +68,89 @@ callsInto(f, m, n) calling f ends up calling m's n
58
68
  ```
59
69
 
60
70
  `resolves` is the question most callers ask. `comesTo` is the one
61
- underneath it, and it can answer with an object, since a chain has to
62
- pass through objects for `routes.list` to reach what `list` holds.
71
+ underneath it, and it can come back with an object, because a chain has
72
+ to pass through objects for `routes.list` to reach whatever `list`
73
+ contains.
63
74
 
64
75
  `isWrittenAs` follows the same names to the expression a value is
65
76
  written as, whatever kind of expression that turns out to be. A GraphQL
66
77
  document is neither a function nor an object, so `comesTo` never
67
78
  reaches one.
68
79
 
69
- `comesFrom` answers the direction `comesTo` cannot. Every `comesTo`
70
- chain ends at something written out in the source being read, so a name
71
- for a library's own function ends nowhere: the library's body is not
72
- here. `comesFrom` follows the same aliases, imports and barrels and
73
- answers with the module and the name that module exports.
80
+ `comesFrom` covers the direction `comesTo` cannot. Every `comesTo` chain
81
+ ends at something written out in the source suss is reading, so a name
82
+ for a library's own function ends nowhere, because the library's body is
83
+ not here. `comesFrom` follows the same aliases, imports and barrels, and
84
+ comes back with the module plus the name that module exports.
74
85
 
75
86
  `callsInto` puts that together with the calls a function makes. A
76
87
  project writes its own decorator that calls `Resolver()` and applies
77
88
  that one to its classes, and the class is a resolver even though nothing
78
- about it says `Resolver`. Several answers for one function is the normal
79
- case rather than an ambiguity, since a wrapper composing two library
80
- decorators applies both, so a caller asks whether the one it cares about
81
- is among them.
89
+ about it says `Resolver`. Getting several answers for one function is
90
+ normal rather than ambiguous, because a wrapper that combines two
91
+ library decorators applies both of them, so a caller asks whether the
92
+ one it cares about is among them.
82
93
 
83
- One relation comes out for the rules' own use rather than for callers:
94
+ One relation exists for the rules' own use rather than for callers:
84
95
 
85
96
  ```
86
97
  objectOf(x, obj) x stands for the object literal obj
87
98
  ```
88
99
 
89
100
  An object arrives two ways, through a name or as what a factory call
90
- returns, and `objectOf` names that step so the rule for `routes.list`
91
- and the rule for `make(body).handle` are the same rule. A factory call
92
- gets an `objectOf` answer without getting a `comesTo` answer, since a
93
- factory call usually is the wrapper and answering with the function it
94
- returns would fight the unwrapping answer.
101
+ returns. `objectOf` gives that step a name, so the rule for
102
+ `routes.list` and the rule for `make(body).handle` are the same rule. A
103
+ factory call gets an `objectOf` answer without getting a `comesTo`
104
+ answer, because a factory call is usually the wrapper itself, and
105
+ answering with the function it returns would fight the unwrapping
106
+ answer.
95
107
 
96
108
  ## Why rules and not a walker
97
109
 
98
110
  Each rule describes one hop. The chains people write are longer than
99
111
  that, and nobody writes a rule for them: a factory handing off to
100
112
  another factory, a closure three levels down calling the argument, a
101
- barrel re-exporting a wrapper. The engine composes what it has, so those
102
- work without being named.
113
+ barrel re-exporting a wrapper. The engine composes what it has, so all
114
+ of those work without anyone writing a rule for them.
103
115
 
104
- The corollary is where to look when something comes back empty. Suspect
105
- the facts before the rules. Against one production service suss
116
+ That tells you where to look when something comes back empty. Suspect
117
+ the facts before the rules. Against one production service, suss
106
118
  resolved 11 handlers and missed most of what the template declared. The
107
119
  fix was one condition in fact extraction, and the existing rules found
108
- the rest unchanged.
120
+ the rest without any change.
109
121
 
110
122
  ## What is not modelled
111
123
 
112
124
  **A handler passed as one property of a config.** `make({ body:
113
- handler })`, where the factory reads `opts.body`, does not resolve. A
114
- rule for it was tried and taken out: a wrapper often reads several
125
+ handler })`, where the factory reads `opts.body`, does not resolve. We
126
+ tried a rule for it and took it back out. A wrapper often reads several
115
127
  callbacks off the same config object, the rule made each one a
116
- candidate for the whole call, and the resulting ambiguity nulled out
117
- handlers that used to resolve. Saying which property is the handler
118
- takes something structure does not carry,
119
- either "the only property called" (which needs negation) or a pack
120
- naming the property. Until one of those exists, this shape stays
128
+ candidate for the whole call, and the ambiguity that produced nulled out
129
+ handlers that used to resolve. Working out which property is the handler
130
+ needs something the structure does not tell you: either "the only
131
+ property that gets called" (which needs negation) or a pack that says
132
+ which property it is. Until one of those exists, this pattern stays
121
133
  unresolved on purpose.
122
134
 
123
135
  **An element of an array.** `all[0]` has no fact for what an array
124
- holds.
136
+ contains.
125
137
 
126
138
  **Which write a read sees, once control flow decides it.** A name
127
- written twice in a module's own statement list is answered: those
139
+ written twice in a module's own statement list does resolve. Those
128
140
  statements run once each, top to bottom, so the last write is what
129
141
  anything importing the name gets, and the adapter says so with
130
142
  `endsHolding`. A write inside a branch, a loop, or a function body is a
131
- different claim, and the adapter stays quiet. The name then comes to
132
- nothing, which is the answer until there are control-flow facts to
133
- reason over.
143
+ different claim, and the adapter says nothing about it. The name then
144
+ resolves to nothing, and it stays that way until we have control-flow
145
+ facts to reason over.
134
146
 
135
- Answering it in general is reaching definitions, per use rather than
147
+ Answering that in general is reaching definitions, per use rather than
136
148
  per name. That needs facts saying which statement follows which and
137
- which branch each sits on, and no adapter emits any today.
149
+ which branch each one is in, and no adapter emits any of those today.
138
150
 
139
- The adapter picking the write rather than a rule picking it is a cost
140
- decision. Ordering writes inside the rules means asking which writes
141
- have no later write, negation says that in one line, and this
151
+ We have the adapter pick the write rather than a rule, and that is a
152
+ cost decision. Ordering writes inside the rules means asking which
153
+ writes have no later write. Negation says that in one line, but this
142
154
  evaluator has to throw its last fixpoint away and start over whenever a
143
155
  rule set uses negation. The store evaluates after every wave of facts,
144
156
  so one negated rule turned a 66 second run on the Saleor dashboard into
@@ -146,8 +158,8 @@ one that had not finished in ten minutes. Source order is something
146
158
  every adapter already knows.
147
159
 
148
160
  **Ambiguity is the caller's problem.** When the rules reach two
149
- different functions the store returns nothing, since picking one would
150
- make the answer depend on the order facts arrived in.
161
+ different functions, the store returns nothing, because picking one
162
+ would make the answer depend on the order the facts arrived in.
151
163
 
152
164
  ## License
153
165
 
package/dist/index.cjs CHANGED
@@ -21,10 +21,166 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ANSWER_RELATIONS: () => ANSWER_RELATIONS,
24
+ FACT_CONTRACT_CASES: () => FACT_CONTRACT_CASES,
24
25
  RESOLUTION_QUESTIONS: () => RESOLUTION_QUESTIONS,
25
- RESOLUTION_RULES: () => RESOLUTION_RULES
26
+ RESOLUTION_RULES: () => RESOLUTION_RULES,
27
+ checkFactContract: () => checkFactContract
26
28
  });
27
29
  module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/contract.ts
32
+ var distinct = (values) => new Set(values).size;
33
+ var RELATIONS_THAT_CAN_CROSS = ["imports", "binds", "reExports"];
34
+ var FACT_CONTRACT_CASES = [
35
+ {
36
+ name: "two functions, one parameter name",
37
+ requires: "two functions in one file, each declaring a parameter of the same name",
38
+ check: (facts) => {
39
+ const params = facts("paramOf").map((row) => row[2] ?? "");
40
+ if (params.length < 2) {
41
+ return `expected two parameters, found ${params.length}`;
42
+ }
43
+ return distinct(params) === params.length ? null : "two parameters share a key, so a value is keyed by its name rather than by the node that declares it";
44
+ }
45
+ },
46
+ {
47
+ name: "a name read inside a function",
48
+ requires: "a function with a parameter, whose body reads that parameter and returns it",
49
+ // An adapter may key the read as the parameter itself, or as its own
50
+ // node linked by `binds`. Both join; what fails is a read that reaches
51
+ // its declaration by neither.
52
+ check: (facts) => {
53
+ const param = facts("paramOf")[0]?.[2];
54
+ const returned = facts("returnsValue")[0]?.[1];
55
+ if (param === void 0 || returned === void 0) {
56
+ return "expected one parameter and one return";
57
+ }
58
+ if (param === returned) {
59
+ return null;
60
+ }
61
+ const linked = facts("binds").some(
62
+ (row) => row[0] === returned && row[1] === param
63
+ );
64
+ return linked ? null : "a read of a parameter is neither the parameter nor bound to it, so a read and its declaration never join";
65
+ }
66
+ },
67
+ {
68
+ name: "a name bound to a call",
69
+ requires: "a module-level name assigned the result of a call",
70
+ check: (facts) => {
71
+ const call = facts("call")[0]?.[0];
72
+ if (call === void 0) {
73
+ return "expected one call";
74
+ }
75
+ const written = facts("writtenValue").some((row) => row[0] === call);
76
+ if (!written) {
77
+ return "a call is not a written value, so a chain ending at one derives nothing through isWrittenAs";
78
+ }
79
+ return facts("comesTo").some((row) => row[0] === call) ? "a call has a comesTo, which the rules withhold on purpose so a factory call does not answer with what it returns" : null;
80
+ }
81
+ },
82
+ {
83
+ name: "a written-out sequence",
84
+ requires: "a module-level name assigned a written-out list of two names",
85
+ check: (facts) => {
86
+ if (facts("objectValue").length === 0) {
87
+ return "a sequence is not an object value, so nothing can be read out of it";
88
+ }
89
+ const keys = facts("holdsProperty").map((row) => row[1] ?? "");
90
+ if (keys.length < 2) {
91
+ return `a sequence keeps no elements: expected two, found ${keys.length}`;
92
+ }
93
+ return keys.includes("0") && keys.includes("1") ? null : `a sequence keeps its elements under ${JSON.stringify(keys)} rather than their positions, so one property rule cannot cover indexed access`;
94
+ }
95
+ },
96
+ {
97
+ name: "a module exporting a name",
98
+ requires: "a module-level function declaration",
99
+ check: (facts) => {
100
+ const exported = facts("exportsAs")[0];
101
+ if (exported === void 0) {
102
+ return "a module exports nothing, so no other file can reach into it";
103
+ }
104
+ const value = exported[2] ?? "";
105
+ return facts("func").some((row) => row[0] === value) ? null : "what a module exports is not the node that declares it";
106
+ }
107
+ },
108
+ {
109
+ name: "a class declaring a method",
110
+ requires: "a module-level class with one method, exported where a language says so",
111
+ check: (facts) => {
112
+ const objects = new Set(facts("objectValue").map((row) => row[0]));
113
+ if (objects.size === 0) {
114
+ return "a class is not an object value, so nothing can be read off an instance of it";
115
+ }
116
+ const functions = new Set(facts("func").map((row) => row[0]));
117
+ const contained = facts("holdsProperty").filter(
118
+ (row) => objects.has(row[0])
119
+ );
120
+ if (contained.length === 0) {
121
+ return "a class contains none of its methods, so a method read off an instance resolves to nothing";
122
+ }
123
+ return contained.some((row) => functions.has(row[2])) ? null : "what a class contains under a method name is not the node that declares the method";
124
+ }
125
+ },
126
+ {
127
+ name: "a value another file declares",
128
+ requires: "two files, one declaring a value and the other reading it by name",
129
+ // Which relation says so is the language's business. TypeScript and Python
130
+ // both write an import; Ruby has none and binds the reading site straight
131
+ // to the definition. What has to be true is that some fact reaches out of
132
+ // the file doing the reading into the file doing the declaring.
133
+ check: (facts, files) => {
134
+ if (files.length < 2) {
135
+ return "expected two files";
136
+ }
137
+ const mentions = (value, file) => value.includes(file);
138
+ const crosses = RELATIONS_THAT_CAN_CROSS.some(
139
+ (relation) => facts(relation).some(
140
+ (row) => files.some(
141
+ (from) => mentions(row[0] ?? "", from) && row.slice(1).some(
142
+ (cell) => files.some((to) => to !== from && mentions(cell, to))
143
+ )
144
+ )
145
+ )
146
+ );
147
+ return crosses ? null : "nothing links a name read in one file to what another file declares, so a value cannot leave the file it is written in";
148
+ }
149
+ }
150
+ ];
151
+ function checkFactContract(sources, emit, options = {}) {
152
+ return FACT_CONTRACT_CASES.reduce(async (soFar, contractCase) => {
153
+ const failures = await soFar;
154
+ const files = sources[contractCase.name];
155
+ if (files === void 0) {
156
+ return [
157
+ ...failures,
158
+ {
159
+ case: contractCase.name,
160
+ problem: `no source supplied for: ${contractCase.requires}`
161
+ }
162
+ ];
163
+ }
164
+ const db = await emit(files);
165
+ const problem = contractCase.check(
166
+ (relation) => db.facts(relation).map((row) => row.map(String)),
167
+ Object.keys(files)
168
+ );
169
+ const known = options.known?.[contractCase.name];
170
+ if (known !== void 0) {
171
+ return problem === null ? [
172
+ ...failures,
173
+ {
174
+ case: contractCase.name,
175
+ problem: `written down as a known gap (${known}) and now satisfied, so take it off the list`
176
+ }
177
+ ] : failures;
178
+ }
179
+ return problem === null ? failures : [...failures, { case: contractCase.name, problem }];
180
+ }, Promise.resolve([]));
181
+ }
182
+
183
+ // src/index.ts
28
184
  var import_datalog = require("@suss/datalog");
29
185
  var RESOLUTION_RULES = [
30
186
  // A value comes to itself; every chain ends at something written out
@@ -37,7 +193,7 @@ var RESOLUTION_RULES = [
37
193
  [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("z")],
38
194
  [(0, import_datalog.lit)("binds", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("y")), (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("y"), (0, import_datalog.variable)("z"))]
39
195
  ),
40
- // A name written more than once holds the value the last write left
196
+ // A name written more than once has the value the last write left
41
197
  // there, and `binds` says nothing about it. The adapter works out
42
198
  // which write that is, in its own language's terms, and stays quiet
43
199
  // when control flow decides. A name it stays quiet about comes to no
@@ -79,7 +235,7 @@ var RESOLUTION_RULES = [
79
235
  (0, import_datalog.lit)("moduleExport", (0, import_datalog.variable)("m2"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("value"))
80
236
  ]
81
237
  ),
82
- // The object an expression stands for. A name arrives at one by
238
+ // The object an expression refers to. A name arrives at one by
83
239
  // following `comesTo` through aliases and imports. A factory call
84
240
  // arrives at one through what the function it calls returns; the
85
241
  // call itself is given no `comesTo`, since a factory call usually IS
@@ -101,6 +257,92 @@ var RESOLUTION_RULES = [
101
257
  (0, import_datalog.lit)("objectValue", (0, import_datalog.variable)("obj"))
102
258
  ]
103
259
  ),
260
+ // Which calls a function, found by the name the call is written as
261
+ // rather than by resolving every callee in the project. A caller knows
262
+ // the function and wants its call sites, so both of these start from
263
+ // the function. Starting from `call` instead asks what every call in
264
+ // the project imports, which was 72% of everything derived.
265
+ (0, import_datalog.rule)(
266
+ "callsFunction",
267
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
268
+ [(0, import_datalog.lit)("binds", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("f")), (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))]
269
+ ),
270
+ (0, import_datalog.rule)(
271
+ "callsFunction",
272
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
273
+ [
274
+ (0, import_datalog.lit)("moduleExport", (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("f")),
275
+ (0, import_datalog.lit)("imports", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n")),
276
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))
277
+ ]
278
+ ),
279
+ // A function whose return is itself a call arrives at the same object
280
+ // that call does, so a chain passing through two factories settles.
281
+ (0, import_datalog.rule)(
282
+ "objectOf",
283
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("obj")],
284
+ [
285
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
286
+ (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("f")),
287
+ (0, import_datalog.lit)("returnsValue", (0, import_datalog.variable)("f"), (0, import_datalog.variable)("ret")),
288
+ (0, import_datalog.lit)("objectOf", (0, import_datalog.variable)("ret"), (0, import_datalog.variable)("obj"))
289
+ ]
290
+ ),
291
+ // An argument reaches the parameter it is passed to, by position or
292
+ // by the name the caller wrote. A function called from several places
293
+ // leaves its parameter with more than one value, and the caller
294
+ // decides what to do about that.
295
+ (0, import_datalog.rule)(
296
+ "comesTo",
297
+ [(0, import_datalog.variable)("p"), (0, import_datalog.variable)("z")],
298
+ [
299
+ (0, import_datalog.lit)("paramOf", (0, import_datalog.variable)("f"), (0, import_datalog.variable)("k"), (0, import_datalog.variable)("p")),
300
+ (0, import_datalog.lit)("callsFunction", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")),
301
+ (0, import_datalog.lit)("callArg", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("k"), (0, import_datalog.variable)("a")),
302
+ (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("a"), (0, import_datalog.variable)("z"))
303
+ ]
304
+ ),
305
+ (0, import_datalog.rule)(
306
+ "comesTo",
307
+ [(0, import_datalog.variable)("p"), (0, import_datalog.variable)("z")],
308
+ [
309
+ (0, import_datalog.lit)("paramNamed", (0, import_datalog.variable)("f"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("p")),
310
+ (0, import_datalog.lit)("callsFunction", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")),
311
+ (0, import_datalog.lit)("callKeywordArg", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("a")),
312
+ (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("a"), (0, import_datalog.variable)("z"))
313
+ ]
314
+ ),
315
+ // What an object contains, its base class included, so a method the base
316
+ // declares is found on a subclass that never overrode it. A method both
317
+ // declare gives two, and the caller decides. This is its own relation
318
+ // rather than more `holdsProperty`, which stays something an adapter
319
+ // states and the rules only read.
320
+ (0, import_datalog.rule)(
321
+ "contains",
322
+ [(0, import_datalog.variable)("o"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")],
323
+ [(0, import_datalog.lit)("holdsProperty", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held"))]
324
+ ),
325
+ (0, import_datalog.rule)(
326
+ "contains",
327
+ [(0, import_datalog.variable)("cls"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")],
328
+ [
329
+ (0, import_datalog.lit)("extends", (0, import_datalog.variable)("cls"), (0, import_datalog.variable)("base")),
330
+ (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("base"), (0, import_datalog.variable)("baseCls")),
331
+ (0, import_datalog.lit)("contains", (0, import_datalog.variable)("baseCls"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held"))
332
+ ]
333
+ ),
334
+ // Calling a class makes one of it, and reading a method off the
335
+ // result finds the method the class declares. The caveat above is
336
+ // about a factory function, and a class is not one.
337
+ (0, import_datalog.rule)(
338
+ "comesTo",
339
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("cls")],
340
+ [
341
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
342
+ (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("cls")),
343
+ (0, import_datalog.lit)("objectValue", (0, import_datalog.variable)("cls"))
344
+ ]
345
+ ),
104
346
  // Reading a property comes to what the object holds under that name,
105
347
  // whichever way the object arrived: `routes.list` off a name, or
106
348
  // `make(body).handle` off a call.
@@ -110,7 +352,7 @@ var RESOLUTION_RULES = [
110
352
  [
111
353
  (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), (0, import_datalog.variable)("n")),
112
354
  (0, import_datalog.lit)("objectOf", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("obj")),
113
- (0, import_datalog.lit)("holdsProperty", (0, import_datalog.variable)("obj"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")),
355
+ (0, import_datalog.lit)("contains", (0, import_datalog.variable)("obj"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")),
114
356
  (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("held"), (0, import_datalog.variable)("z"))
115
357
  ]
116
358
  ),
@@ -203,7 +445,7 @@ var RESOLUTION_RULES = [
203
445
  (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("a"), (0, import_datalog.variable)("h"))
204
446
  ]
205
447
  ),
206
- // Where a name comes from, when what it names lives outside the
448
+ // Where a name comes from, when what it refers to lives outside the
207
449
  // source being read. `comesTo` ends at something written out, so it
208
450
  // never reaches a library's own function, and a project's alias of a
209
451
  // library name has no answer at all. This follows the same aliases
@@ -314,6 +556,14 @@ var RESOLUTION_QUESTIONS = [
314
556
  [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("z")],
315
557
  [(0, import_datalog.lit)("wanted", (0, import_datalog.variable)("x")), (0, import_datalog.lit)("isWrittenAs", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("z"))]
316
558
  ),
559
+ // A call is given no `comesTo`, so this is the only way to ask what
560
+ // object one arrives at, and without it a demand-driven run derives
561
+ // `objectOf` nowhere.
562
+ (0, import_datalog.rule)(
563
+ "wantedObjectOf",
564
+ [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("z")],
565
+ [(0, import_datalog.lit)("wanted", (0, import_datalog.variable)("x")), (0, import_datalog.lit)("objectOf", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("z"))]
566
+ ),
317
567
  (0, import_datalog.rule)(
318
568
  "wantedComesFrom",
319
569
  [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n")],
@@ -335,6 +585,8 @@ var ANSWER_RELATIONS = [
335
585
  // Annotate the CommonJS export names for ESM import in node:
336
586
  0 && (module.exports = {
337
587
  ANSWER_RELATIONS,
588
+ FACT_CONTRACT_CASES,
338
589
  RESOLUTION_QUESTIONS,
339
- RESOLUTION_RULES
590
+ RESOLUTION_RULES,
591
+ checkFactContract
340
592
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,40 @@
1
1
  import * as _suss_datalog from '@suss/datalog';
2
+ import { Database } from '@suss/datalog';
3
+
4
+ /** The tuples of one relation, as strings, which is all a case compares. */
5
+ type FactsOf = (relation: string) => string[][];
6
+ interface ContractCase {
7
+ /** The key an adapter supplies its own source under. */
8
+ readonly name: string;
9
+ /** What that source has to contain, for whoever writes it. */
10
+ readonly requires: string;
11
+ /**
12
+ * Null when the facts satisfy the case; otherwise what is wrong with them.
13
+ * `files` is the source file names the adapter supplied, so a case about a
14
+ * value leaving its file can say which file it should have left.
15
+ */
16
+ readonly check: (facts: FactsOf, files: readonly string[]) => string | null;
17
+ }
18
+ declare const FACT_CONTRACT_CASES: readonly ContractCase[];
19
+ /** One case's source, as the files it takes, because a value leaving its file takes two. */
20
+ type CaseFiles = Readonly<Record<string, string>>;
21
+ interface ContractOptions {
22
+ /**
23
+ * Cases the adapter is known not to satisfy, each with why. A known case
24
+ * that starts passing is reported too, so a gap that gets closed does not
25
+ * stay written down as one.
26
+ */
27
+ readonly known?: Readonly<Record<string, string>>;
28
+ }
29
+ /**
30
+ * Every case an adapter fails, with what is wrong. Empty when the adapter
31
+ * satisfies the contract. `emit` reads the files a case supplies and gives
32
+ * back the facts the adapter produced for them.
33
+ */
34
+ declare function checkFactContract(sources: Readonly<Record<string, CaseFiles>>, emit: (files: CaseFiles) => Promise<Database> | Database, options?: ContractOptions): Promise<{
35
+ case: string;
36
+ problem: string;
37
+ }[]>;
2
38
 
3
39
  /**
4
40
  * The rules every language adapter shares. Concatenate a language's own
@@ -8,7 +44,7 @@ declare const RESOLUTION_RULES: _suss_datalog.Rule[];
8
44
  /**
9
45
  * The questions a caller asks, written as rules. Two facts say somebody
10
46
  * is asking: `wanted(x)` for what a value is, and `wantedOrigin(x)` for
11
- * where a name came from. Each answer relation holds the pairs for the
47
+ * where a name came from. Each answer relation contains the pairs for the
12
48
  * values somebody asked about, keyed by the value asked about, which is
13
49
  * the key the caller looks up by anyway.
14
50
  *
@@ -26,4 +62,4 @@ declare const RESOLUTION_QUESTIONS: _suss_datalog.Rule[];
26
62
  /** The relations `RESOLUTION_QUESTIONS` answers into. */
27
63
  declare const ANSWER_RELATIONS: string[];
28
64
 
29
- export { ANSWER_RELATIONS, RESOLUTION_QUESTIONS, RESOLUTION_RULES };
65
+ export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, FACT_CONTRACT_CASES, type FactsOf, RESOLUTION_QUESTIONS, RESOLUTION_RULES, checkFactContract };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,40 @@
1
1
  import * as _suss_datalog from '@suss/datalog';
2
+ import { Database } from '@suss/datalog';
3
+
4
+ /** The tuples of one relation, as strings, which is all a case compares. */
5
+ type FactsOf = (relation: string) => string[][];
6
+ interface ContractCase {
7
+ /** The key an adapter supplies its own source under. */
8
+ readonly name: string;
9
+ /** What that source has to contain, for whoever writes it. */
10
+ readonly requires: string;
11
+ /**
12
+ * Null when the facts satisfy the case; otherwise what is wrong with them.
13
+ * `files` is the source file names the adapter supplied, so a case about a
14
+ * value leaving its file can say which file it should have left.
15
+ */
16
+ readonly check: (facts: FactsOf, files: readonly string[]) => string | null;
17
+ }
18
+ declare const FACT_CONTRACT_CASES: readonly ContractCase[];
19
+ /** One case's source, as the files it takes, because a value leaving its file takes two. */
20
+ type CaseFiles = Readonly<Record<string, string>>;
21
+ interface ContractOptions {
22
+ /**
23
+ * Cases the adapter is known not to satisfy, each with why. A known case
24
+ * that starts passing is reported too, so a gap that gets closed does not
25
+ * stay written down as one.
26
+ */
27
+ readonly known?: Readonly<Record<string, string>>;
28
+ }
29
+ /**
30
+ * Every case an adapter fails, with what is wrong. Empty when the adapter
31
+ * satisfies the contract. `emit` reads the files a case supplies and gives
32
+ * back the facts the adapter produced for them.
33
+ */
34
+ declare function checkFactContract(sources: Readonly<Record<string, CaseFiles>>, emit: (files: CaseFiles) => Promise<Database> | Database, options?: ContractOptions): Promise<{
35
+ case: string;
36
+ problem: string;
37
+ }[]>;
2
38
 
3
39
  /**
4
40
  * The rules every language adapter shares. Concatenate a language's own
@@ -8,7 +44,7 @@ declare const RESOLUTION_RULES: _suss_datalog.Rule[];
8
44
  /**
9
45
  * The questions a caller asks, written as rules. Two facts say somebody
10
46
  * is asking: `wanted(x)` for what a value is, and `wantedOrigin(x)` for
11
- * where a name came from. Each answer relation holds the pairs for the
47
+ * where a name came from. Each answer relation contains the pairs for the
12
48
  * values somebody asked about, keyed by the value asked about, which is
13
49
  * the key the caller looks up by anyway.
14
50
  *
@@ -26,4 +62,4 @@ declare const RESOLUTION_QUESTIONS: _suss_datalog.Rule[];
26
62
  /** The relations `RESOLUTION_QUESTIONS` answers into. */
27
63
  declare const ANSWER_RELATIONS: string[];
28
64
 
29
- export { ANSWER_RELATIONS, RESOLUTION_QUESTIONS, RESOLUTION_RULES };
65
+ export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, FACT_CONTRACT_CASES, type FactsOf, RESOLUTION_QUESTIONS, RESOLUTION_RULES, checkFactContract };
package/dist/index.js CHANGED
@@ -1,3 +1,155 @@
1
+ // src/contract.ts
2
+ var distinct = (values) => new Set(values).size;
3
+ var RELATIONS_THAT_CAN_CROSS = ["imports", "binds", "reExports"];
4
+ var FACT_CONTRACT_CASES = [
5
+ {
6
+ name: "two functions, one parameter name",
7
+ requires: "two functions in one file, each declaring a parameter of the same name",
8
+ check: (facts) => {
9
+ const params = facts("paramOf").map((row) => row[2] ?? "");
10
+ if (params.length < 2) {
11
+ return `expected two parameters, found ${params.length}`;
12
+ }
13
+ return distinct(params) === params.length ? null : "two parameters share a key, so a value is keyed by its name rather than by the node that declares it";
14
+ }
15
+ },
16
+ {
17
+ name: "a name read inside a function",
18
+ requires: "a function with a parameter, whose body reads that parameter and returns it",
19
+ // An adapter may key the read as the parameter itself, or as its own
20
+ // node linked by `binds`. Both join; what fails is a read that reaches
21
+ // its declaration by neither.
22
+ check: (facts) => {
23
+ const param = facts("paramOf")[0]?.[2];
24
+ const returned = facts("returnsValue")[0]?.[1];
25
+ if (param === void 0 || returned === void 0) {
26
+ return "expected one parameter and one return";
27
+ }
28
+ if (param === returned) {
29
+ return null;
30
+ }
31
+ const linked = facts("binds").some(
32
+ (row) => row[0] === returned && row[1] === param
33
+ );
34
+ return linked ? null : "a read of a parameter is neither the parameter nor bound to it, so a read and its declaration never join";
35
+ }
36
+ },
37
+ {
38
+ name: "a name bound to a call",
39
+ requires: "a module-level name assigned the result of a call",
40
+ check: (facts) => {
41
+ const call = facts("call")[0]?.[0];
42
+ if (call === void 0) {
43
+ return "expected one call";
44
+ }
45
+ const written = facts("writtenValue").some((row) => row[0] === call);
46
+ if (!written) {
47
+ return "a call is not a written value, so a chain ending at one derives nothing through isWrittenAs";
48
+ }
49
+ return facts("comesTo").some((row) => row[0] === call) ? "a call has a comesTo, which the rules withhold on purpose so a factory call does not answer with what it returns" : null;
50
+ }
51
+ },
52
+ {
53
+ name: "a written-out sequence",
54
+ requires: "a module-level name assigned a written-out list of two names",
55
+ check: (facts) => {
56
+ if (facts("objectValue").length === 0) {
57
+ return "a sequence is not an object value, so nothing can be read out of it";
58
+ }
59
+ const keys = facts("holdsProperty").map((row) => row[1] ?? "");
60
+ if (keys.length < 2) {
61
+ return `a sequence keeps no elements: expected two, found ${keys.length}`;
62
+ }
63
+ return keys.includes("0") && keys.includes("1") ? null : `a sequence keeps its elements under ${JSON.stringify(keys)} rather than their positions, so one property rule cannot cover indexed access`;
64
+ }
65
+ },
66
+ {
67
+ name: "a module exporting a name",
68
+ requires: "a module-level function declaration",
69
+ check: (facts) => {
70
+ const exported = facts("exportsAs")[0];
71
+ if (exported === void 0) {
72
+ return "a module exports nothing, so no other file can reach into it";
73
+ }
74
+ const value = exported[2] ?? "";
75
+ return facts("func").some((row) => row[0] === value) ? null : "what a module exports is not the node that declares it";
76
+ }
77
+ },
78
+ {
79
+ name: "a class declaring a method",
80
+ requires: "a module-level class with one method, exported where a language says so",
81
+ check: (facts) => {
82
+ const objects = new Set(facts("objectValue").map((row) => row[0]));
83
+ if (objects.size === 0) {
84
+ return "a class is not an object value, so nothing can be read off an instance of it";
85
+ }
86
+ const functions = new Set(facts("func").map((row) => row[0]));
87
+ const contained = facts("holdsProperty").filter(
88
+ (row) => objects.has(row[0])
89
+ );
90
+ if (contained.length === 0) {
91
+ return "a class contains none of its methods, so a method read off an instance resolves to nothing";
92
+ }
93
+ return contained.some((row) => functions.has(row[2])) ? null : "what a class contains under a method name is not the node that declares the method";
94
+ }
95
+ },
96
+ {
97
+ name: "a value another file declares",
98
+ requires: "two files, one declaring a value and the other reading it by name",
99
+ // Which relation says so is the language's business. TypeScript and Python
100
+ // both write an import; Ruby has none and binds the reading site straight
101
+ // to the definition. What has to be true is that some fact reaches out of
102
+ // the file doing the reading into the file doing the declaring.
103
+ check: (facts, files) => {
104
+ if (files.length < 2) {
105
+ return "expected two files";
106
+ }
107
+ const mentions = (value, file) => value.includes(file);
108
+ const crosses = RELATIONS_THAT_CAN_CROSS.some(
109
+ (relation) => facts(relation).some(
110
+ (row) => files.some(
111
+ (from) => mentions(row[0] ?? "", from) && row.slice(1).some(
112
+ (cell) => files.some((to) => to !== from && mentions(cell, to))
113
+ )
114
+ )
115
+ )
116
+ );
117
+ return crosses ? null : "nothing links a name read in one file to what another file declares, so a value cannot leave the file it is written in";
118
+ }
119
+ }
120
+ ];
121
+ function checkFactContract(sources, emit, options = {}) {
122
+ return FACT_CONTRACT_CASES.reduce(async (soFar, contractCase) => {
123
+ const failures = await soFar;
124
+ const files = sources[contractCase.name];
125
+ if (files === void 0) {
126
+ return [
127
+ ...failures,
128
+ {
129
+ case: contractCase.name,
130
+ problem: `no source supplied for: ${contractCase.requires}`
131
+ }
132
+ ];
133
+ }
134
+ const db = await emit(files);
135
+ const problem = contractCase.check(
136
+ (relation) => db.facts(relation).map((row) => row.map(String)),
137
+ Object.keys(files)
138
+ );
139
+ const known = options.known?.[contractCase.name];
140
+ if (known !== void 0) {
141
+ return problem === null ? [
142
+ ...failures,
143
+ {
144
+ case: contractCase.name,
145
+ problem: `written down as a known gap (${known}) and now satisfied, so take it off the list`
146
+ }
147
+ ] : failures;
148
+ }
149
+ return problem === null ? failures : [...failures, { case: contractCase.name, problem }];
150
+ }, Promise.resolve([]));
151
+ }
152
+
1
153
  // src/index.ts
2
154
  import { lit, rule, variable as v } from "@suss/datalog";
3
155
  var RESOLUTION_RULES = [
@@ -11,7 +163,7 @@ var RESOLUTION_RULES = [
11
163
  [v("x"), v("z")],
12
164
  [lit("binds", v("x"), v("y")), lit("comesTo", v("y"), v("z"))]
13
165
  ),
14
- // A name written more than once holds the value the last write left
166
+ // A name written more than once has the value the last write left
15
167
  // there, and `binds` says nothing about it. The adapter works out
16
168
  // which write that is, in its own language's terms, and stays quiet
17
169
  // when control flow decides. A name it stays quiet about comes to no
@@ -53,7 +205,7 @@ var RESOLUTION_RULES = [
53
205
  lit("moduleExport", v("m2"), v("n"), v("value"))
54
206
  ]
55
207
  ),
56
- // The object an expression stands for. A name arrives at one by
208
+ // The object an expression refers to. A name arrives at one by
57
209
  // following `comesTo` through aliases and imports. A factory call
58
210
  // arrives at one through what the function it calls returns; the
59
211
  // call itself is given no `comesTo`, since a factory call usually IS
@@ -75,6 +227,92 @@ var RESOLUTION_RULES = [
75
227
  lit("objectValue", v("obj"))
76
228
  ]
77
229
  ),
230
+ // Which calls a function, found by the name the call is written as
231
+ // rather than by resolving every callee in the project. A caller knows
232
+ // the function and wants its call sites, so both of these start from
233
+ // the function. Starting from `call` instead asks what every call in
234
+ // the project imports, which was 72% of everything derived.
235
+ rule(
236
+ "callsFunction",
237
+ [v("r"), v("f")],
238
+ [lit("binds", v("c"), v("f")), lit("call", v("r"), v("c"))]
239
+ ),
240
+ rule(
241
+ "callsFunction",
242
+ [v("r"), v("f")],
243
+ [
244
+ lit("moduleExport", v("m"), v("n"), v("f")),
245
+ lit("imports", v("c"), v("m"), v("n")),
246
+ lit("call", v("r"), v("c"))
247
+ ]
248
+ ),
249
+ // A function whose return is itself a call arrives at the same object
250
+ // that call does, so a chain passing through two factories settles.
251
+ rule(
252
+ "objectOf",
253
+ [v("r"), v("obj")],
254
+ [
255
+ lit("call", v("r"), v("c")),
256
+ lit("comesTo", v("c"), v("f")),
257
+ lit("returnsValue", v("f"), v("ret")),
258
+ lit("objectOf", v("ret"), v("obj"))
259
+ ]
260
+ ),
261
+ // An argument reaches the parameter it is passed to, by position or
262
+ // by the name the caller wrote. A function called from several places
263
+ // leaves its parameter with more than one value, and the caller
264
+ // decides what to do about that.
265
+ rule(
266
+ "comesTo",
267
+ [v("p"), v("z")],
268
+ [
269
+ lit("paramOf", v("f"), v("k"), v("p")),
270
+ lit("callsFunction", v("r"), v("f")),
271
+ lit("callArg", v("r"), v("k"), v("a")),
272
+ lit("comesTo", v("a"), v("z"))
273
+ ]
274
+ ),
275
+ rule(
276
+ "comesTo",
277
+ [v("p"), v("z")],
278
+ [
279
+ lit("paramNamed", v("f"), v("n"), v("p")),
280
+ lit("callsFunction", v("r"), v("f")),
281
+ lit("callKeywordArg", v("r"), v("n"), v("a")),
282
+ lit("comesTo", v("a"), v("z"))
283
+ ]
284
+ ),
285
+ // What an object contains, its base class included, so a method the base
286
+ // declares is found on a subclass that never overrode it. A method both
287
+ // declare gives two, and the caller decides. This is its own relation
288
+ // rather than more `holdsProperty`, which stays something an adapter
289
+ // states and the rules only read.
290
+ rule(
291
+ "contains",
292
+ [v("o"), v("n"), v("held")],
293
+ [lit("holdsProperty", v("o"), v("n"), v("held"))]
294
+ ),
295
+ rule(
296
+ "contains",
297
+ [v("cls"), v("n"), v("held")],
298
+ [
299
+ lit("extends", v("cls"), v("base")),
300
+ lit("comesTo", v("base"), v("baseCls")),
301
+ lit("contains", v("baseCls"), v("n"), v("held"))
302
+ ]
303
+ ),
304
+ // Calling a class makes one of it, and reading a method off the
305
+ // result finds the method the class declares. The caveat above is
306
+ // about a factory function, and a class is not one.
307
+ rule(
308
+ "comesTo",
309
+ [v("r"), v("cls")],
310
+ [
311
+ lit("call", v("r"), v("c")),
312
+ lit("comesTo", v("c"), v("cls")),
313
+ lit("objectValue", v("cls"))
314
+ ]
315
+ ),
78
316
  // Reading a property comes to what the object holds under that name,
79
317
  // whichever way the object arrived: `routes.list` off a name, or
80
318
  // `make(body).handle` off a call.
@@ -84,7 +322,7 @@ var RESOLUTION_RULES = [
84
322
  [
85
323
  lit("readsProperty", v("x"), v("o"), v("n")),
86
324
  lit("objectOf", v("o"), v("obj")),
87
- lit("holdsProperty", v("obj"), v("n"), v("held")),
325
+ lit("contains", v("obj"), v("n"), v("held")),
88
326
  lit("comesTo", v("held"), v("z"))
89
327
  ]
90
328
  ),
@@ -177,7 +415,7 @@ var RESOLUTION_RULES = [
177
415
  lit("comesTo", v("a"), v("h"))
178
416
  ]
179
417
  ),
180
- // Where a name comes from, when what it names lives outside the
418
+ // Where a name comes from, when what it refers to lives outside the
181
419
  // source being read. `comesTo` ends at something written out, so it
182
420
  // never reaches a library's own function, and a project's alias of a
183
421
  // library name has no answer at all. This follows the same aliases
@@ -288,6 +526,14 @@ var RESOLUTION_QUESTIONS = [
288
526
  [v("x"), v("z")],
289
527
  [lit("wanted", v("x")), lit("isWrittenAs", v("x"), v("z"))]
290
528
  ),
529
+ // A call is given no `comesTo`, so this is the only way to ask what
530
+ // object one arrives at, and without it a demand-driven run derives
531
+ // `objectOf` nowhere.
532
+ rule(
533
+ "wantedObjectOf",
534
+ [v("x"), v("z")],
535
+ [lit("wanted", v("x")), lit("objectOf", v("x"), v("z"))]
536
+ ),
291
537
  rule(
292
538
  "wantedComesFrom",
293
539
  [v("x"), v("m"), v("n")],
@@ -308,6 +554,8 @@ var ANSWER_RELATIONS = [
308
554
  ];
309
555
  export {
310
556
  ANSWER_RELATIONS,
557
+ FACT_CONTRACT_CASES,
311
558
  RESOLUTION_QUESTIONS,
312
- RESOLUTION_RULES
559
+ RESOLUTION_RULES,
560
+ checkFactContract
313
561
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/resolution",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Language-neutral rules for following a value to the function it comes down to.",
6
6
  "keywords": [
@@ -53,6 +53,6 @@
53
53
  "access": "public"
54
54
  },
55
55
  "dependencies": {
56
- "@suss/datalog": "0.3.2"
56
+ "@suss/datalog": "0.5.0"
57
57
  }
58
58
  }