@suss/resolution 0.10.0 → 0.12.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 +38 -0
- package/dist/index.cjs +507 -221
- package/dist/index.d.cts +71 -2
- package/dist/index.d.ts +71 -2
- package/dist/index.js +297 -13
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _suss_datalog from '@suss/datalog';
|
|
2
|
-
import { Database } from '@suss/datalog';
|
|
2
|
+
import { Database, Atom, Tuple, Proof } from '@suss/datalog';
|
|
3
3
|
|
|
4
4
|
/** The tuples of one relation, as strings, which is all a case compares. */
|
|
5
5
|
type FactsOf = (relation: string) => string[][];
|
|
@@ -36,6 +36,75 @@ declare function checkFactContract(sources: Readonly<Record<string, CaseFiles>>,
|
|
|
36
36
|
problem: string;
|
|
37
37
|
}[]>;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Turn a witness proof over the resolution rules into the chain a
|
|
41
|
+
* person reads: which value led to which, one reason per hop, with the
|
|
42
|
+
* assumptions the chain rests on called out.
|
|
43
|
+
*
|
|
44
|
+
* This module knows which rules are hops and which are plumbing: a
|
|
45
|
+
* `stepsTo` firing is a hop somebody should see, a `reaches` firing is
|
|
46
|
+
* the closure gluing hops together, and the base cases ("x is already
|
|
47
|
+
* a function") end a chain without adding to it. Everything here is
|
|
48
|
+
* about the resolution rules; the engine stays prose-free. Atoms in a
|
|
49
|
+
* proof are the adapter's node ids, so every sentence goes through a
|
|
50
|
+
* caller-supplied `describe` to say them in source terms.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** How a caller says an atom in source terms: a name, a file, a line. */
|
|
54
|
+
type DescribeAtom = (atom: Atom) => string;
|
|
55
|
+
/** One hop of a chain, with the sentence saying why it is true. */
|
|
56
|
+
interface ResolutionStep {
|
|
57
|
+
from: Atom;
|
|
58
|
+
to: Atom;
|
|
59
|
+
/** The rule that fired, by the name it was given. */
|
|
60
|
+
rule: string;
|
|
61
|
+
/** Why this hop is true, in one sentence. */
|
|
62
|
+
reason: string;
|
|
63
|
+
/** Detail under the hop: barrel re-exports, a callee's own chain. */
|
|
64
|
+
notes: string[];
|
|
65
|
+
/** What the hop rests on beyond what the source says. */
|
|
66
|
+
assumptions: string[];
|
|
67
|
+
}
|
|
68
|
+
interface ResolutionExplanation {
|
|
69
|
+
/** The chain's atoms in order, the asked-about value first. */
|
|
70
|
+
atoms: Atom[];
|
|
71
|
+
steps: ResolutionStep[];
|
|
72
|
+
/** All assumptions from every step, in chain order. */
|
|
73
|
+
assumptions: string[];
|
|
74
|
+
/** True when the depth cap or the cycle guard stopped the walk. */
|
|
75
|
+
truncated: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** What a phrase reads: the fired rule's premises, spoken through `describe`. */
|
|
78
|
+
interface StepContext {
|
|
79
|
+
tuple: Tuple;
|
|
80
|
+
premises: readonly Proof[];
|
|
81
|
+
describe: DescribeAtom;
|
|
82
|
+
/** Render a nested proof's own hops, for a callee with a chain of its own. */
|
|
83
|
+
inline: (proof: Proof) => string[];
|
|
84
|
+
}
|
|
85
|
+
type StepPhrase = (context: StepContext) => {
|
|
86
|
+
reason: string;
|
|
87
|
+
notes?: string[];
|
|
88
|
+
assumptions?: string[];
|
|
89
|
+
};
|
|
90
|
+
interface ExplainOptions {
|
|
91
|
+
describe: DescribeAtom;
|
|
92
|
+
/** Phrases for a caller's own step rules, over the built-in ones. */
|
|
93
|
+
phrases?: Record<string, StepPhrase>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Flatten a proof of one resolution answer into its chain. Returns
|
|
97
|
+
* null for a proof that is not over these rules: an absence (the fact
|
|
98
|
+
* was never derived), or some other rule set's relation.
|
|
99
|
+
*/
|
|
100
|
+
declare function explainResolutionProof(proof: Proof, options: ExplainOptions): ResolutionExplanation | null;
|
|
101
|
+
/**
|
|
102
|
+
* The explanation as lines a report prints: the chain first, then one
|
|
103
|
+
* reason per hop with its notes indented under it, then what the chain
|
|
104
|
+
* assumes. A truncated walk says so instead of trailing off.
|
|
105
|
+
*/
|
|
106
|
+
declare function renderExplanation(explanation: ResolutionExplanation, describe: DescribeAtom): string[];
|
|
107
|
+
|
|
39
108
|
/** A step to the value x is written as. */
|
|
40
109
|
declare const VALUE_STEP: _suss_datalog.Term;
|
|
41
110
|
/** A step to what running the call x is handed back. */
|
|
@@ -76,4 +145,4 @@ declare const RESOLUTION_QUESTIONS: _suss_datalog.Rule[];
|
|
|
76
145
|
/** The relations `RESOLUTION_QUESTIONS` answers into. */
|
|
77
146
|
declare const ANSWER_RELATIONS: string[];
|
|
78
147
|
|
|
79
|
-
export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, FACT_CONTRACT_CASES, type FactsOf, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, VALUE_STEP, checkFactContract };
|
|
148
|
+
export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, type DescribeAtom, type ExplainOptions, FACT_CONTRACT_CASES, type FactsOf, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, type ResolutionExplanation, type ResolutionStep, type StepContext, type StepPhrase, VALUE_STEP, checkFactContract, explainResolutionProof, renderExplanation };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _suss_datalog from '@suss/datalog';
|
|
2
|
-
import { Database } from '@suss/datalog';
|
|
2
|
+
import { Database, Atom, Tuple, Proof } from '@suss/datalog';
|
|
3
3
|
|
|
4
4
|
/** The tuples of one relation, as strings, which is all a case compares. */
|
|
5
5
|
type FactsOf = (relation: string) => string[][];
|
|
@@ -36,6 +36,75 @@ declare function checkFactContract(sources: Readonly<Record<string, CaseFiles>>,
|
|
|
36
36
|
problem: string;
|
|
37
37
|
}[]>;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Turn a witness proof over the resolution rules into the chain a
|
|
41
|
+
* person reads: which value led to which, one reason per hop, with the
|
|
42
|
+
* assumptions the chain rests on called out.
|
|
43
|
+
*
|
|
44
|
+
* This module knows which rules are hops and which are plumbing: a
|
|
45
|
+
* `stepsTo` firing is a hop somebody should see, a `reaches` firing is
|
|
46
|
+
* the closure gluing hops together, and the base cases ("x is already
|
|
47
|
+
* a function") end a chain without adding to it. Everything here is
|
|
48
|
+
* about the resolution rules; the engine stays prose-free. Atoms in a
|
|
49
|
+
* proof are the adapter's node ids, so every sentence goes through a
|
|
50
|
+
* caller-supplied `describe` to say them in source terms.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** How a caller says an atom in source terms: a name, a file, a line. */
|
|
54
|
+
type DescribeAtom = (atom: Atom) => string;
|
|
55
|
+
/** One hop of a chain, with the sentence saying why it is true. */
|
|
56
|
+
interface ResolutionStep {
|
|
57
|
+
from: Atom;
|
|
58
|
+
to: Atom;
|
|
59
|
+
/** The rule that fired, by the name it was given. */
|
|
60
|
+
rule: string;
|
|
61
|
+
/** Why this hop is true, in one sentence. */
|
|
62
|
+
reason: string;
|
|
63
|
+
/** Detail under the hop: barrel re-exports, a callee's own chain. */
|
|
64
|
+
notes: string[];
|
|
65
|
+
/** What the hop rests on beyond what the source says. */
|
|
66
|
+
assumptions: string[];
|
|
67
|
+
}
|
|
68
|
+
interface ResolutionExplanation {
|
|
69
|
+
/** The chain's atoms in order, the asked-about value first. */
|
|
70
|
+
atoms: Atom[];
|
|
71
|
+
steps: ResolutionStep[];
|
|
72
|
+
/** All assumptions from every step, in chain order. */
|
|
73
|
+
assumptions: string[];
|
|
74
|
+
/** True when the depth cap or the cycle guard stopped the walk. */
|
|
75
|
+
truncated: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** What a phrase reads: the fired rule's premises, spoken through `describe`. */
|
|
78
|
+
interface StepContext {
|
|
79
|
+
tuple: Tuple;
|
|
80
|
+
premises: readonly Proof[];
|
|
81
|
+
describe: DescribeAtom;
|
|
82
|
+
/** Render a nested proof's own hops, for a callee with a chain of its own. */
|
|
83
|
+
inline: (proof: Proof) => string[];
|
|
84
|
+
}
|
|
85
|
+
type StepPhrase = (context: StepContext) => {
|
|
86
|
+
reason: string;
|
|
87
|
+
notes?: string[];
|
|
88
|
+
assumptions?: string[];
|
|
89
|
+
};
|
|
90
|
+
interface ExplainOptions {
|
|
91
|
+
describe: DescribeAtom;
|
|
92
|
+
/** Phrases for a caller's own step rules, over the built-in ones. */
|
|
93
|
+
phrases?: Record<string, StepPhrase>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Flatten a proof of one resolution answer into its chain. Returns
|
|
97
|
+
* null for a proof that is not over these rules: an absence (the fact
|
|
98
|
+
* was never derived), or some other rule set's relation.
|
|
99
|
+
*/
|
|
100
|
+
declare function explainResolutionProof(proof: Proof, options: ExplainOptions): ResolutionExplanation | null;
|
|
101
|
+
/**
|
|
102
|
+
* The explanation as lines a report prints: the chain first, then one
|
|
103
|
+
* reason per hop with its notes indented under it, then what the chain
|
|
104
|
+
* assumes. A truncated walk says so instead of trailing off.
|
|
105
|
+
*/
|
|
106
|
+
declare function renderExplanation(explanation: ResolutionExplanation, describe: DescribeAtom): string[];
|
|
107
|
+
|
|
39
108
|
/** A step to the value x is written as. */
|
|
40
109
|
declare const VALUE_STEP: _suss_datalog.Term;
|
|
41
110
|
/** A step to what running the call x is handed back. */
|
|
@@ -76,4 +145,4 @@ declare const RESOLUTION_QUESTIONS: _suss_datalog.Rule[];
|
|
|
76
145
|
/** The relations `RESOLUTION_QUESTIONS` answers into. */
|
|
77
146
|
declare const ANSWER_RELATIONS: string[];
|
|
78
147
|
|
|
79
|
-
export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, FACT_CONTRACT_CASES, type FactsOf, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, VALUE_STEP, checkFactContract };
|
|
148
|
+
export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, type DescribeAtom, type ExplainOptions, FACT_CONTRACT_CASES, type FactsOf, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, type ResolutionExplanation, type ResolutionStep, type StepContext, type StepPhrase, VALUE_STEP, checkFactContract, explainResolutionProof, renderExplanation };
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,27 @@ var FACT_CONTRACT_CASES = [
|
|
|
113
113
|
return facts("callArg").length > 0 ? null : "a construction passes its argument nowhere, so the parameter it lands in has no value";
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
|
+
{
|
|
117
|
+
name: "a name declared as a fallback",
|
|
118
|
+
requires: "a module-level name declared as a fallback between a property read and a call, `cached || build()` say",
|
|
119
|
+
// The fallback expression states its branches; each branch has to be
|
|
120
|
+
// keyed as the value node itself, or the chain stops at the fallback
|
|
121
|
+
// instead of continuing into whichever branch resolves.
|
|
122
|
+
check: (facts) => {
|
|
123
|
+
const branches = facts("fallbackBranch");
|
|
124
|
+
if (branches.length < 2) {
|
|
125
|
+
return `expected a fallback with two branches, found ${branches.length}`;
|
|
126
|
+
}
|
|
127
|
+
if (distinct(branches.map((row) => row[0] ?? "")) !== 1) {
|
|
128
|
+
return "the two branches are keyed under different expressions, so nothing says they are alternatives for one value";
|
|
129
|
+
}
|
|
130
|
+
const call = facts("call")[0]?.[0];
|
|
131
|
+
if (call === void 0) {
|
|
132
|
+
return "the call branch is not written down as a call, so the branch that resolves has nothing behind it";
|
|
133
|
+
}
|
|
134
|
+
return branches.some((row) => row[1] === call) ? null : "no branch is the call node itself, so the chain cannot continue into the branch that resolves";
|
|
135
|
+
}
|
|
136
|
+
},
|
|
116
137
|
{
|
|
117
138
|
name: "a value another file declares",
|
|
118
139
|
requires: "two files, one declaring a value and the other reading it by name",
|
|
@@ -170,6 +191,242 @@ function checkFactContract(sources, emit, options = {}) {
|
|
|
170
191
|
}, Promise.resolve([]));
|
|
171
192
|
}
|
|
172
193
|
|
|
194
|
+
// src/explain.ts
|
|
195
|
+
import { ruleLabel } from "@suss/datalog";
|
|
196
|
+
function exportTrail(proof, describe) {
|
|
197
|
+
if (proof.kind !== "derived") {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const name = ruleLabel(proof.rule);
|
|
201
|
+
if (name === "re-export") {
|
|
202
|
+
const [m, n, m2, n2] = proof.premises[0].tuple;
|
|
203
|
+
return [
|
|
204
|
+
`${describe(m)} takes ${String(n)} from ${describe(m2)}, where it is called ${String(n2)}`,
|
|
205
|
+
...exportTrail(proof.premises[1], describe)
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
if (name === "re-export all") {
|
|
209
|
+
const [m, m2] = proof.premises[0].tuple;
|
|
210
|
+
return [
|
|
211
|
+
`${describe(m)} forwards everything ${describe(m2)} exports`,
|
|
212
|
+
...exportTrail(proof.premises[1], describe)
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
function argumentReason(proof, describe) {
|
|
218
|
+
const [, p, a] = proof.tuple;
|
|
219
|
+
if (proof.kind !== "derived") {
|
|
220
|
+
return `${describe(p)} is a parameter, and a call passes it ${describe(a)}`;
|
|
221
|
+
}
|
|
222
|
+
const f = proof.premises[0].tuple[0];
|
|
223
|
+
return `${describe(p)} is a parameter of ${describe(f)}, and a call passes it ${describe(a)}`;
|
|
224
|
+
}
|
|
225
|
+
var DEFAULT_PHRASES = {
|
|
226
|
+
alias: ({ tuple, describe }) => ({
|
|
227
|
+
reason: `${describe(tuple[0])} is declared as ${describe(tuple[1])}`
|
|
228
|
+
}),
|
|
229
|
+
"last write": ({ tuple, describe }) => ({
|
|
230
|
+
reason: `${describe(tuple[0])} is written more than once, and the last write leaves it as ${describe(tuple[1])}`
|
|
231
|
+
}),
|
|
232
|
+
fallback: ({ tuple, describe }) => ({
|
|
233
|
+
reason: `${describe(tuple[0])} is a fallback expression, and ${describe(tuple[1])} is the branch that resolves`
|
|
234
|
+
}),
|
|
235
|
+
import: ({ tuple, premises, describe }) => {
|
|
236
|
+
const [, module, name] = premises[0].tuple;
|
|
237
|
+
return {
|
|
238
|
+
reason: `${describe(tuple[0])} is imported from ${describe(module)} under the name ${String(name)}`,
|
|
239
|
+
notes: exportTrail(premises[1], describe)
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
argument: ({ premises, describe }) => ({
|
|
243
|
+
reason: argumentReason(premises[0], describe)
|
|
244
|
+
}),
|
|
245
|
+
"property read": ({ tuple, premises, describe }) => {
|
|
246
|
+
const [, object, name] = premises[0].tuple;
|
|
247
|
+
const contains = premises[2];
|
|
248
|
+
const inherited = contains.kind === "derived" && contains.premises.length > 1;
|
|
249
|
+
return {
|
|
250
|
+
reason: `${describe(tuple[0])} reads ${String(name)} off ${describe(object)}, which contains ${describe(tuple[1])}`,
|
|
251
|
+
notes: inherited ? [`${String(name)} comes from a base class`] : []
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
"class instance": ({ tuple, premises, describe, inline }) => ({
|
|
255
|
+
reason: `${describe(tuple[0])} makes an instance of ${describe(tuple[1])}`,
|
|
256
|
+
notes: inline(premises[1])
|
|
257
|
+
}),
|
|
258
|
+
"factory unwrap": ({ tuple, premises, describe, inline }) => {
|
|
259
|
+
const factory = premises[1].tuple[1];
|
|
260
|
+
return {
|
|
261
|
+
reason: `${describe(tuple[0])} calls ${describe(factory)}, a factory that passes its argument through, so it comes down to ${describe(tuple[1])}`,
|
|
262
|
+
notes: inline(premises[1])
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
"declared wrapper": ({ tuple, premises, describe }) => {
|
|
266
|
+
const callee = String(premises[0].tuple[1]);
|
|
267
|
+
const module = String(premises[2].tuple[1]);
|
|
268
|
+
const argument = String(premises[1].tuple[1]);
|
|
269
|
+
return {
|
|
270
|
+
reason: `${describe(tuple[0])} calls ${callee} from ${module}, which passes argument ${argument} through, so it comes down to ${describe(tuple[1])}`,
|
|
271
|
+
assumptions: [
|
|
272
|
+
`a pack declares that ${callee} from ${module} passes argument ${argument} through to its result`
|
|
273
|
+
]
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
"call result": ({ tuple, premises, describe, inline }) => {
|
|
277
|
+
const invokes = premises[0];
|
|
278
|
+
const invoked = invokes.tuple[1];
|
|
279
|
+
const calleeWalks = invokes.kind === "derived" ? invokes.premises.filter(
|
|
280
|
+
(premise) => premise.kind === "derived" && (premise.relation === "comesTo" || premise.relation === "givesBack")
|
|
281
|
+
) : [];
|
|
282
|
+
return {
|
|
283
|
+
reason: `${describe(tuple[0])} runs ${describe(invoked)}, which returns ${describe(tuple[1])}`,
|
|
284
|
+
notes: calleeWalks.flatMap((premise) => inline(premise))
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var CHAIN_RELATIONS = /* @__PURE__ */ new Set([
|
|
289
|
+
"resolves",
|
|
290
|
+
"comesTo",
|
|
291
|
+
"givesBack",
|
|
292
|
+
"isWrittenAs",
|
|
293
|
+
"comesFrom",
|
|
294
|
+
"reaches",
|
|
295
|
+
"stepsTo"
|
|
296
|
+
]);
|
|
297
|
+
function inlineChain(proof, state) {
|
|
298
|
+
const explained = explainResolutionProof(proof, {
|
|
299
|
+
describe: state.describe,
|
|
300
|
+
phrases: state.phrases
|
|
301
|
+
});
|
|
302
|
+
if (explained === null || explained.steps.length === 0) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
if (explained.truncated) {
|
|
306
|
+
state.truncated = true;
|
|
307
|
+
}
|
|
308
|
+
return explained.steps.map((step) => step.reason);
|
|
309
|
+
}
|
|
310
|
+
function stepFrom(proof, state) {
|
|
311
|
+
const name = ruleLabel(proof.rule);
|
|
312
|
+
const phrase = state.phrases[name];
|
|
313
|
+
const from = proof.tuple[0];
|
|
314
|
+
const to = proof.tuple[1];
|
|
315
|
+
if (phrase === void 0) {
|
|
316
|
+
return {
|
|
317
|
+
from,
|
|
318
|
+
to,
|
|
319
|
+
rule: name,
|
|
320
|
+
reason: `${state.describe(from)} steps to ${state.describe(to)} (${name})`,
|
|
321
|
+
notes: [],
|
|
322
|
+
assumptions: []
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const said = phrase({
|
|
326
|
+
tuple: proof.tuple,
|
|
327
|
+
premises: proof.premises,
|
|
328
|
+
describe: state.describe,
|
|
329
|
+
inline: (nested) => inlineChain(nested, state)
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
from,
|
|
333
|
+
to,
|
|
334
|
+
rule: name,
|
|
335
|
+
reason: said.reason,
|
|
336
|
+
notes: said.notes ?? [],
|
|
337
|
+
assumptions: said.assumptions ?? []
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function importStep(proof, state) {
|
|
341
|
+
const [x, module, name] = proof.tuple;
|
|
342
|
+
return {
|
|
343
|
+
from: x,
|
|
344
|
+
to: module,
|
|
345
|
+
rule: "import",
|
|
346
|
+
reason: `${state.describe(x)} is imported from ${state.describe(module)} under the name ${String(name)}`,
|
|
347
|
+
notes: [],
|
|
348
|
+
assumptions: []
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
var flattenInto = (proof, state) => {
|
|
352
|
+
if (proof.kind === "truncated") {
|
|
353
|
+
state.truncated = true;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (proof.kind !== "derived") {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const relation = proof.relation;
|
|
360
|
+
if (relation === "stepsTo") {
|
|
361
|
+
state.steps.push(stepFrom(proof, state));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (relation === "reaches") {
|
|
365
|
+
for (const p of proof.premises) {
|
|
366
|
+
flattenInto(p, state);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (relation === "comesFrom") {
|
|
371
|
+
if (proof.premises.length === 2) {
|
|
372
|
+
flattenInto(proof.premises[0], state);
|
|
373
|
+
state.steps.push(importStep(proof.premises[1], state));
|
|
374
|
+
} else {
|
|
375
|
+
state.steps.push(importStep(proof.premises[0], state));
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
for (const p of proof.premises) {
|
|
380
|
+
if (p.kind === "derived" && (p.relation === "reaches" || p.relation === "comesTo")) {
|
|
381
|
+
flattenInto(p, state);
|
|
382
|
+
}
|
|
383
|
+
if (p.kind === "truncated") {
|
|
384
|
+
state.truncated = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
function explainResolutionProof(proof, options) {
|
|
389
|
+
if (proof.kind === "absence") {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (proof.kind === "derived" && !CHAIN_RELATIONS.has(proof.relation)) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const state = {
|
|
396
|
+
steps: [],
|
|
397
|
+
truncated: proof.kind === "truncated",
|
|
398
|
+
describe: options.describe,
|
|
399
|
+
phrases: { ...DEFAULT_PHRASES, ...options.phrases }
|
|
400
|
+
};
|
|
401
|
+
flattenInto(proof, state);
|
|
402
|
+
const atoms = proof.kind === "fact" || proof.kind === "truncated" ? [proof.tuple[0]] : [proof.tuple[0], ...state.steps.map((step) => step.to)];
|
|
403
|
+
return {
|
|
404
|
+
atoms,
|
|
405
|
+
steps: state.steps,
|
|
406
|
+
assumptions: state.steps.flatMap((step) => step.assumptions),
|
|
407
|
+
truncated: state.truncated
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function renderExplanation(explanation, describe) {
|
|
411
|
+
const lines = [];
|
|
412
|
+
if (explanation.steps.length > 0) {
|
|
413
|
+
lines.push(explanation.atoms.map((atom) => describe(atom)).join(" -> "));
|
|
414
|
+
}
|
|
415
|
+
for (const step of explanation.steps) {
|
|
416
|
+
lines.push(` ${step.reason}`);
|
|
417
|
+
for (const note of step.notes) {
|
|
418
|
+
lines.push(` ${note}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
for (const assumption of explanation.assumptions) {
|
|
422
|
+
lines.push(` assuming ${assumption}`);
|
|
423
|
+
}
|
|
424
|
+
if (explanation.truncated) {
|
|
425
|
+
lines.push(" the chain goes on past the proof depth cap");
|
|
426
|
+
}
|
|
427
|
+
return lines;
|
|
428
|
+
}
|
|
429
|
+
|
|
173
430
|
// src/index.ts
|
|
174
431
|
import { constant, lit, rule, variable as v } from "@suss/datalog";
|
|
175
432
|
var VALUE_STEP = constant("value");
|
|
@@ -178,14 +435,29 @@ var RESOLUTION_RULES = [
|
|
|
178
435
|
// Aliasing: const x = y, or an identifier referencing a declaration.
|
|
179
436
|
// A language with a hop of its own, like JavaScript's `.bind`, states
|
|
180
437
|
// it as a step too, or every question but `comesTo` misses it.
|
|
181
|
-
rule(
|
|
438
|
+
rule(
|
|
439
|
+
"stepsTo",
|
|
440
|
+
[v("x"), v("y"), VALUE_STEP],
|
|
441
|
+
[lit("binds", v("x"), v("y"))],
|
|
442
|
+
"alias"
|
|
443
|
+
),
|
|
182
444
|
// A name written more than once has the value the last write left
|
|
183
445
|
// there. The adapter works out which write that is and stays quiet
|
|
184
446
|
// when control flow decides, so such a name steps nowhere at all.
|
|
185
447
|
rule(
|
|
186
448
|
"stepsTo",
|
|
187
449
|
[v("x"), v("y"), VALUE_STEP],
|
|
188
|
-
[lit("endsHolding", v("x"), v("y"))]
|
|
450
|
+
[lit("endsHolding", v("x"), v("y"))],
|
|
451
|
+
"last write"
|
|
452
|
+
),
|
|
453
|
+
// A fallback says the value is one of its branches, so each branch is
|
|
454
|
+
// a step. A branch that resolves to nothing makes no claim, and two
|
|
455
|
+
// branches resolving to different things fail the single-answer policy.
|
|
456
|
+
rule(
|
|
457
|
+
"stepsTo",
|
|
458
|
+
[v("x"), v("b"), VALUE_STEP],
|
|
459
|
+
[lit("fallbackBranch", v("x"), v("b"))],
|
|
460
|
+
"fallback"
|
|
189
461
|
),
|
|
190
462
|
// An import steps to what the module exports under that name.
|
|
191
463
|
rule(
|
|
@@ -194,7 +466,8 @@ var RESOLUTION_RULES = [
|
|
|
194
466
|
[
|
|
195
467
|
lit("imports", v("x"), v("m"), v("n")),
|
|
196
468
|
lit("moduleExport", v("m"), v("n"), v("value"))
|
|
197
|
-
]
|
|
469
|
+
],
|
|
470
|
+
"import"
|
|
198
471
|
),
|
|
199
472
|
// A parameter steps to what a call passes it. A function called from
|
|
200
473
|
// several places leaves its parameter with more than one value, and a
|
|
@@ -202,7 +475,8 @@ var RESOLUTION_RULES = [
|
|
|
202
475
|
rule(
|
|
203
476
|
"stepsTo",
|
|
204
477
|
[v("p"), v("a"), VALUE_STEP],
|
|
205
|
-
[lit("passesArgument", v("r"), v("p"), v("a"))]
|
|
478
|
+
[lit("passesArgument", v("r"), v("p"), v("a"))],
|
|
479
|
+
"argument"
|
|
206
480
|
),
|
|
207
481
|
// Reading a property steps to what the object contains under that
|
|
208
482
|
// name, whichever way the object arrived: `routes.list` off a name,
|
|
@@ -214,7 +488,8 @@ var RESOLUTION_RULES = [
|
|
|
214
488
|
lit("readsProperty", v("x"), v("o"), v("n")),
|
|
215
489
|
lit("objectOf", v("o"), v("obj")),
|
|
216
490
|
lit("contains", v("obj"), v("n"), v("held"))
|
|
217
|
-
]
|
|
491
|
+
],
|
|
492
|
+
"property read"
|
|
218
493
|
),
|
|
219
494
|
// Calling a class makes one of it, so the call steps to the class and
|
|
220
495
|
// a method read off the result is the one the class declares. The
|
|
@@ -226,7 +501,8 @@ var RESOLUTION_RULES = [
|
|
|
226
501
|
lit("call", v("r"), v("c")),
|
|
227
502
|
lit("comesTo", v("c"), v("cls")),
|
|
228
503
|
lit("objectValue", v("cls"))
|
|
229
|
-
]
|
|
504
|
+
],
|
|
505
|
+
"class instance"
|
|
230
506
|
),
|
|
231
507
|
// Wrapper transparency, derived: calling a factory that returns a
|
|
232
508
|
// function which calls its parameter k steps to argument k.
|
|
@@ -238,7 +514,8 @@ var RESOLUTION_RULES = [
|
|
|
238
514
|
lit("comesTo", v("c"), v("f")),
|
|
239
515
|
lit("unwraps", v("f"), v("k")),
|
|
240
516
|
lit("callArg", v("r"), v("k"), v("a"))
|
|
241
|
-
]
|
|
517
|
+
],
|
|
518
|
+
"factory unwrap"
|
|
242
519
|
),
|
|
243
520
|
// Wrapper transparency, declared: a pack says this callee wraps
|
|
244
521
|
// argument k. The callee has to come from the library the pack said,
|
|
@@ -252,14 +529,16 @@ var RESOLUTION_RULES = [
|
|
|
252
529
|
lit("wrapperModule", v("n"), v("m")),
|
|
253
530
|
lit("calleeOrigin", v("r"), v("m")),
|
|
254
531
|
lit("callArg", v("r"), v("k"), v("a"))
|
|
255
|
-
]
|
|
532
|
+
],
|
|
533
|
+
"declared wrapper"
|
|
256
534
|
),
|
|
257
535
|
// The one step that runs a function forwards: a call steps to what
|
|
258
536
|
// the function it invokes returns.
|
|
259
537
|
rule(
|
|
260
538
|
"stepsTo",
|
|
261
539
|
[v("r"), v("ret"), RESULT_STEP],
|
|
262
|
-
[lit("invokes", v("r"), v("f")), lit("returnsValue", v("f"), v("ret"))]
|
|
540
|
+
[lit("invokes", v("r"), v("f")), lit("returnsValue", v("f"), v("ret"))],
|
|
541
|
+
"call result"
|
|
263
542
|
),
|
|
264
543
|
// Where a walk gets to, and whether it ran a call on the way. A value
|
|
265
544
|
// step keeps whatever the rest of the walk was, and a result step
|
|
@@ -374,7 +653,8 @@ var RESOLUTION_RULES = [
|
|
|
374
653
|
rule(
|
|
375
654
|
"moduleExport",
|
|
376
655
|
[v("m"), v("n"), v("value")],
|
|
377
|
-
[lit("exportsAs", v("m"), v("n"), v("value"))]
|
|
656
|
+
[lit("exportsAs", v("m"), v("n"), v("value"))],
|
|
657
|
+
"export"
|
|
378
658
|
),
|
|
379
659
|
rule(
|
|
380
660
|
"moduleExport",
|
|
@@ -382,7 +662,8 @@ var RESOLUTION_RULES = [
|
|
|
382
662
|
[
|
|
383
663
|
lit("reExports", v("m"), v("n"), v("m2"), v("n2")),
|
|
384
664
|
lit("moduleExport", v("m2"), v("n2"), v("value"))
|
|
385
|
-
]
|
|
665
|
+
],
|
|
666
|
+
"re-export"
|
|
386
667
|
),
|
|
387
668
|
rule(
|
|
388
669
|
"moduleExport",
|
|
@@ -390,7 +671,8 @@ var RESOLUTION_RULES = [
|
|
|
390
671
|
[
|
|
391
672
|
lit("reExportsAll", v("m"), v("m2")),
|
|
392
673
|
lit("moduleExport", v("m2"), v("n"), v("value"))
|
|
393
|
-
]
|
|
674
|
+
],
|
|
675
|
+
"re-export all"
|
|
394
676
|
),
|
|
395
677
|
// The function a call runs, written from the call's side because a
|
|
396
678
|
// caller asking what a call gives back has the call in hand.
|
|
@@ -642,5 +924,7 @@ export {
|
|
|
642
924
|
RESOLUTION_RULES,
|
|
643
925
|
RESULT_STEP,
|
|
644
926
|
VALUE_STEP,
|
|
645
|
-
checkFactContract
|
|
927
|
+
checkFactContract,
|
|
928
|
+
explainResolutionProof,
|
|
929
|
+
renderExplanation
|
|
646
930
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/resolution",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.12.0"
|
|
57
57
|
}
|
|
58
58
|
}
|