@suss/resolution 0.3.1 → 0.4.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 +66 -54
- package/dist/index.cjs +257 -6
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +252 -5
- package/package.json +2 -2
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
|
|
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.
|
|
8
|
-
files. An adapter reads source into facts,
|
|
9
|
-
onto these, and evaluates on
|
|
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
|
|
12
|
-
them. A name binds to a value. A call puts an argument in a
|
|
13
|
-
module exports a name and another module can forward it.
|
|
14
|
-
returns a function calling its parameter hands back the
|
|
15
|
-
given, which is a decorator in Python, a closure in Go,
|
|
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)
|
|
45
|
-
`calleeOrigin
|
|
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
|
|
62
|
-
pass through objects for `routes.list` to reach
|
|
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`
|
|
70
|
-
|
|
71
|
-
for a library's own function ends nowhere
|
|
72
|
-
here. `comesFrom` follows the same aliases, imports and barrels and
|
|
73
|
-
|
|
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`.
|
|
79
|
-
|
|
80
|
-
decorators applies both, so a caller asks whether the
|
|
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
|
|
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
|
|
91
|
-
and the rule for `make(body).handle` are the same rule. A
|
|
92
|
-
gets an `objectOf` answer without getting a `comesTo`
|
|
93
|
-
factory call usually
|
|
94
|
-
returns would fight the unwrapping
|
|
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
|
|
102
|
-
work without
|
|
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
|
-
|
|
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
|
|
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.
|
|
114
|
-
rule for it
|
|
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
|
|
117
|
-
handlers that used to resolve.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
132
|
-
nothing,
|
|
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
|
|
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
|
|
149
|
+
which branch each one is in, and no adapter emits any of those today.
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
decision. Ordering writes inside the rules means asking which
|
|
141
|
-
have no later write
|
|
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,
|
|
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
|
|
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
|
|
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,91 @@ 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. Asking what
|
|
262
|
+
// reaches a parameter means looking for call sites, and looking for
|
|
263
|
+
// them the other way round costs a whole-project resolution.
|
|
264
|
+
(0, import_datalog.rule)(
|
|
265
|
+
"callsFunction",
|
|
266
|
+
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
|
|
267
|
+
[(0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")), (0, import_datalog.lit)("binds", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("f"))]
|
|
268
|
+
),
|
|
269
|
+
(0, import_datalog.rule)(
|
|
270
|
+
"callsFunction",
|
|
271
|
+
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
|
|
272
|
+
[
|
|
273
|
+
(0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
|
|
274
|
+
(0, import_datalog.lit)("imports", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n")),
|
|
275
|
+
(0, import_datalog.lit)("moduleExport", (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("f"))
|
|
276
|
+
]
|
|
277
|
+
),
|
|
278
|
+
// A function whose return is itself a call arrives at the same object
|
|
279
|
+
// that call does, so a chain passing through two factories settles.
|
|
280
|
+
(0, import_datalog.rule)(
|
|
281
|
+
"objectOf",
|
|
282
|
+
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("obj")],
|
|
283
|
+
[
|
|
284
|
+
(0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
|
|
285
|
+
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("f")),
|
|
286
|
+
(0, import_datalog.lit)("returnsValue", (0, import_datalog.variable)("f"), (0, import_datalog.variable)("ret")),
|
|
287
|
+
(0, import_datalog.lit)("objectOf", (0, import_datalog.variable)("ret"), (0, import_datalog.variable)("obj"))
|
|
288
|
+
]
|
|
289
|
+
),
|
|
290
|
+
// An argument reaches the parameter it is passed to, by position or
|
|
291
|
+
// by the name the caller wrote. A function called from several places
|
|
292
|
+
// leaves its parameter with more than one value, and the caller
|
|
293
|
+
// decides what to do about that.
|
|
294
|
+
(0, import_datalog.rule)(
|
|
295
|
+
"comesTo",
|
|
296
|
+
[(0, import_datalog.variable)("p"), (0, import_datalog.variable)("z")],
|
|
297
|
+
[
|
|
298
|
+
(0, import_datalog.lit)("paramOf", (0, import_datalog.variable)("f"), (0, import_datalog.variable)("k"), (0, import_datalog.variable)("p")),
|
|
299
|
+
(0, import_datalog.lit)("callsFunction", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")),
|
|
300
|
+
(0, import_datalog.lit)("callArg", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("k"), (0, import_datalog.variable)("a")),
|
|
301
|
+
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("a"), (0, import_datalog.variable)("z"))
|
|
302
|
+
]
|
|
303
|
+
),
|
|
304
|
+
(0, import_datalog.rule)(
|
|
305
|
+
"comesTo",
|
|
306
|
+
[(0, import_datalog.variable)("p"), (0, import_datalog.variable)("z")],
|
|
307
|
+
[
|
|
308
|
+
(0, import_datalog.lit)("paramNamed", (0, import_datalog.variable)("f"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("p")),
|
|
309
|
+
(0, import_datalog.lit)("callsFunction", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")),
|
|
310
|
+
(0, import_datalog.lit)("callKeywordArg", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("a")),
|
|
311
|
+
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("a"), (0, import_datalog.variable)("z"))
|
|
312
|
+
]
|
|
313
|
+
),
|
|
314
|
+
// What an object contains, its base class included, so a method the base
|
|
315
|
+
// declares is found on a subclass that never overrode it. A method both
|
|
316
|
+
// declare gives two, and the caller decides. This is its own relation
|
|
317
|
+
// rather than more `holdsProperty`, which stays something an adapter
|
|
318
|
+
// states and the rules only read.
|
|
319
|
+
(0, import_datalog.rule)(
|
|
320
|
+
"contains",
|
|
321
|
+
[(0, import_datalog.variable)("o"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")],
|
|
322
|
+
[(0, import_datalog.lit)("holdsProperty", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held"))]
|
|
323
|
+
),
|
|
324
|
+
(0, import_datalog.rule)(
|
|
325
|
+
"contains",
|
|
326
|
+
[(0, import_datalog.variable)("cls"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")],
|
|
327
|
+
[
|
|
328
|
+
(0, import_datalog.lit)("extends", (0, import_datalog.variable)("cls"), (0, import_datalog.variable)("base")),
|
|
329
|
+
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("base"), (0, import_datalog.variable)("baseCls")),
|
|
330
|
+
(0, import_datalog.lit)("contains", (0, import_datalog.variable)("baseCls"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held"))
|
|
331
|
+
]
|
|
332
|
+
),
|
|
333
|
+
// Calling a class makes one of it, and reading a method off the
|
|
334
|
+
// result finds the method the class declares. The caveat above is
|
|
335
|
+
// about a factory function, and a class is not one.
|
|
336
|
+
(0, import_datalog.rule)(
|
|
337
|
+
"comesTo",
|
|
338
|
+
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("cls")],
|
|
339
|
+
[
|
|
340
|
+
(0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
|
|
341
|
+
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("cls")),
|
|
342
|
+
(0, import_datalog.lit)("objectValue", (0, import_datalog.variable)("cls"))
|
|
343
|
+
]
|
|
344
|
+
),
|
|
104
345
|
// Reading a property comes to what the object holds under that name,
|
|
105
346
|
// whichever way the object arrived: `routes.list` off a name, or
|
|
106
347
|
// `make(body).handle` off a call.
|
|
@@ -110,7 +351,7 @@ var RESOLUTION_RULES = [
|
|
|
110
351
|
[
|
|
111
352
|
(0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), (0, import_datalog.variable)("n")),
|
|
112
353
|
(0, import_datalog.lit)("objectOf", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("obj")),
|
|
113
|
-
(0, import_datalog.lit)("
|
|
354
|
+
(0, import_datalog.lit)("contains", (0, import_datalog.variable)("obj"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("held")),
|
|
114
355
|
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("held"), (0, import_datalog.variable)("z"))
|
|
115
356
|
]
|
|
116
357
|
),
|
|
@@ -203,7 +444,7 @@ var RESOLUTION_RULES = [
|
|
|
203
444
|
(0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("a"), (0, import_datalog.variable)("h"))
|
|
204
445
|
]
|
|
205
446
|
),
|
|
206
|
-
// Where a name comes from, when what it
|
|
447
|
+
// Where a name comes from, when what it refers to lives outside the
|
|
207
448
|
// source being read. `comesTo` ends at something written out, so it
|
|
208
449
|
// never reaches a library's own function, and a project's alias of a
|
|
209
450
|
// library name has no answer at all. This follows the same aliases
|
|
@@ -314,6 +555,14 @@ var RESOLUTION_QUESTIONS = [
|
|
|
314
555
|
[(0, import_datalog.variable)("x"), (0, import_datalog.variable)("z")],
|
|
315
556
|
[(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
557
|
),
|
|
558
|
+
// A call is given no `comesTo`, so this is the only way to ask what
|
|
559
|
+
// object one arrives at, and without it a demand-driven run derives
|
|
560
|
+
// `objectOf` nowhere.
|
|
561
|
+
(0, import_datalog.rule)(
|
|
562
|
+
"wantedObjectOf",
|
|
563
|
+
[(0, import_datalog.variable)("x"), (0, import_datalog.variable)("z")],
|
|
564
|
+
[(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"))]
|
|
565
|
+
),
|
|
317
566
|
(0, import_datalog.rule)(
|
|
318
567
|
"wantedComesFrom",
|
|
319
568
|
[(0, import_datalog.variable)("x"), (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n")],
|
|
@@ -335,6 +584,8 @@ var ANSWER_RELATIONS = [
|
|
|
335
584
|
// Annotate the CommonJS export names for ESM import in node:
|
|
336
585
|
0 && (module.exports = {
|
|
337
586
|
ANSWER_RELATIONS,
|
|
587
|
+
FACT_CONTRACT_CASES,
|
|
338
588
|
RESOLUTION_QUESTIONS,
|
|
339
|
-
RESOLUTION_RULES
|
|
589
|
+
RESOLUTION_RULES,
|
|
590
|
+
checkFactContract
|
|
340
591
|
});
|
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
|
|
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
|
|
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
|
|
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
|
|
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,91 @@ 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. Asking what
|
|
232
|
+
// reaches a parameter means looking for call sites, and looking for
|
|
233
|
+
// them the other way round costs a whole-project resolution.
|
|
234
|
+
rule(
|
|
235
|
+
"callsFunction",
|
|
236
|
+
[v("r"), v("f")],
|
|
237
|
+
[lit("call", v("r"), v("c")), lit("binds", v("c"), v("f"))]
|
|
238
|
+
),
|
|
239
|
+
rule(
|
|
240
|
+
"callsFunction",
|
|
241
|
+
[v("r"), v("f")],
|
|
242
|
+
[
|
|
243
|
+
lit("call", v("r"), v("c")),
|
|
244
|
+
lit("imports", v("c"), v("m"), v("n")),
|
|
245
|
+
lit("moduleExport", v("m"), v("n"), v("f"))
|
|
246
|
+
]
|
|
247
|
+
),
|
|
248
|
+
// A function whose return is itself a call arrives at the same object
|
|
249
|
+
// that call does, so a chain passing through two factories settles.
|
|
250
|
+
rule(
|
|
251
|
+
"objectOf",
|
|
252
|
+
[v("r"), v("obj")],
|
|
253
|
+
[
|
|
254
|
+
lit("call", v("r"), v("c")),
|
|
255
|
+
lit("comesTo", v("c"), v("f")),
|
|
256
|
+
lit("returnsValue", v("f"), v("ret")),
|
|
257
|
+
lit("objectOf", v("ret"), v("obj"))
|
|
258
|
+
]
|
|
259
|
+
),
|
|
260
|
+
// An argument reaches the parameter it is passed to, by position or
|
|
261
|
+
// by the name the caller wrote. A function called from several places
|
|
262
|
+
// leaves its parameter with more than one value, and the caller
|
|
263
|
+
// decides what to do about that.
|
|
264
|
+
rule(
|
|
265
|
+
"comesTo",
|
|
266
|
+
[v("p"), v("z")],
|
|
267
|
+
[
|
|
268
|
+
lit("paramOf", v("f"), v("k"), v("p")),
|
|
269
|
+
lit("callsFunction", v("r"), v("f")),
|
|
270
|
+
lit("callArg", v("r"), v("k"), v("a")),
|
|
271
|
+
lit("comesTo", v("a"), v("z"))
|
|
272
|
+
]
|
|
273
|
+
),
|
|
274
|
+
rule(
|
|
275
|
+
"comesTo",
|
|
276
|
+
[v("p"), v("z")],
|
|
277
|
+
[
|
|
278
|
+
lit("paramNamed", v("f"), v("n"), v("p")),
|
|
279
|
+
lit("callsFunction", v("r"), v("f")),
|
|
280
|
+
lit("callKeywordArg", v("r"), v("n"), v("a")),
|
|
281
|
+
lit("comesTo", v("a"), v("z"))
|
|
282
|
+
]
|
|
283
|
+
),
|
|
284
|
+
// What an object contains, its base class included, so a method the base
|
|
285
|
+
// declares is found on a subclass that never overrode it. A method both
|
|
286
|
+
// declare gives two, and the caller decides. This is its own relation
|
|
287
|
+
// rather than more `holdsProperty`, which stays something an adapter
|
|
288
|
+
// states and the rules only read.
|
|
289
|
+
rule(
|
|
290
|
+
"contains",
|
|
291
|
+
[v("o"), v("n"), v("held")],
|
|
292
|
+
[lit("holdsProperty", v("o"), v("n"), v("held"))]
|
|
293
|
+
),
|
|
294
|
+
rule(
|
|
295
|
+
"contains",
|
|
296
|
+
[v("cls"), v("n"), v("held")],
|
|
297
|
+
[
|
|
298
|
+
lit("extends", v("cls"), v("base")),
|
|
299
|
+
lit("comesTo", v("base"), v("baseCls")),
|
|
300
|
+
lit("contains", v("baseCls"), v("n"), v("held"))
|
|
301
|
+
]
|
|
302
|
+
),
|
|
303
|
+
// Calling a class makes one of it, and reading a method off the
|
|
304
|
+
// result finds the method the class declares. The caveat above is
|
|
305
|
+
// about a factory function, and a class is not one.
|
|
306
|
+
rule(
|
|
307
|
+
"comesTo",
|
|
308
|
+
[v("r"), v("cls")],
|
|
309
|
+
[
|
|
310
|
+
lit("call", v("r"), v("c")),
|
|
311
|
+
lit("comesTo", v("c"), v("cls")),
|
|
312
|
+
lit("objectValue", v("cls"))
|
|
313
|
+
]
|
|
314
|
+
),
|
|
78
315
|
// Reading a property comes to what the object holds under that name,
|
|
79
316
|
// whichever way the object arrived: `routes.list` off a name, or
|
|
80
317
|
// `make(body).handle` off a call.
|
|
@@ -84,7 +321,7 @@ var RESOLUTION_RULES = [
|
|
|
84
321
|
[
|
|
85
322
|
lit("readsProperty", v("x"), v("o"), v("n")),
|
|
86
323
|
lit("objectOf", v("o"), v("obj")),
|
|
87
|
-
lit("
|
|
324
|
+
lit("contains", v("obj"), v("n"), v("held")),
|
|
88
325
|
lit("comesTo", v("held"), v("z"))
|
|
89
326
|
]
|
|
90
327
|
),
|
|
@@ -177,7 +414,7 @@ var RESOLUTION_RULES = [
|
|
|
177
414
|
lit("comesTo", v("a"), v("h"))
|
|
178
415
|
]
|
|
179
416
|
),
|
|
180
|
-
// Where a name comes from, when what it
|
|
417
|
+
// Where a name comes from, when what it refers to lives outside the
|
|
181
418
|
// source being read. `comesTo` ends at something written out, so it
|
|
182
419
|
// never reaches a library's own function, and a project's alias of a
|
|
183
420
|
// library name has no answer at all. This follows the same aliases
|
|
@@ -288,6 +525,14 @@ var RESOLUTION_QUESTIONS = [
|
|
|
288
525
|
[v("x"), v("z")],
|
|
289
526
|
[lit("wanted", v("x")), lit("isWrittenAs", v("x"), v("z"))]
|
|
290
527
|
),
|
|
528
|
+
// A call is given no `comesTo`, so this is the only way to ask what
|
|
529
|
+
// object one arrives at, and without it a demand-driven run derives
|
|
530
|
+
// `objectOf` nowhere.
|
|
531
|
+
rule(
|
|
532
|
+
"wantedObjectOf",
|
|
533
|
+
[v("x"), v("z")],
|
|
534
|
+
[lit("wanted", v("x")), lit("objectOf", v("x"), v("z"))]
|
|
535
|
+
),
|
|
291
536
|
rule(
|
|
292
537
|
"wantedComesFrom",
|
|
293
538
|
[v("x"), v("m"), v("n")],
|
|
@@ -308,6 +553,8 @@ var ANSWER_RELATIONS = [
|
|
|
308
553
|
];
|
|
309
554
|
export {
|
|
310
555
|
ANSWER_RELATIONS,
|
|
556
|
+
FACT_CONTRACT_CASES,
|
|
311
557
|
RESOLUTION_QUESTIONS,
|
|
312
|
-
RESOLUTION_RULES
|
|
558
|
+
RESOLUTION_RULES,
|
|
559
|
+
checkFactContract
|
|
313
560
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/resolution",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.
|
|
56
|
+
"@suss/datalog": "0.4.0"
|
|
57
57
|
}
|
|
58
58
|
}
|