eyemind 0.1.27 → 0.1.29
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/SPEC.md +6 -3
- package/package.json +1 -1
- package/src/engine.js +37 -33
- package/src/goals.js +22 -4
- package/src/inspection.js +3 -3
- package/src/lang.js +8 -8
- package/src/program.js +22 -21
- package/src/proof.js +17 -33
- package/src/templates.js +8 -12
- package/src/version.js +1 -1
- package/src/writer.js +28 -0
package/SPEC.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
```text
|
|
4
4
|
Title: The eyemind Rule Language and Proof Format
|
|
5
|
-
Version: eyemind 0.1.
|
|
5
|
+
Version: eyemind 0.1.29
|
|
6
6
|
Status: Informational
|
|
7
7
|
Author: Jos De Roo, KNoWS office of IDLab, Ghent University - imec
|
|
8
8
|
Repository: https://github.com/eyereasoner/eyemind
|
|
@@ -22,7 +22,7 @@ command line.
|
|
|
22
22
|
## Status of This Memo
|
|
23
23
|
|
|
24
24
|
This document is not an Internet Standards Track specification. It describes
|
|
25
|
-
the language and formats as implemented by eyemind 0.1.
|
|
25
|
+
the language and formats as implemented by eyemind 0.1.29, for readers who want
|
|
26
26
|
to write programs, produce or consume proofs and reports, or implement a
|
|
27
27
|
compatible reasoner or checker. Where this document and the implementation
|
|
28
28
|
disagree, that is a defect in one of them.
|
|
@@ -340,6 +340,9 @@ only original `implies` and `implied_by` clauses whose heads contain no rule
|
|
|
340
340
|
templates and whose bodies contain no inspection goals, including inside
|
|
341
341
|
controls. Facts, queries, contradictions, template generators and appended
|
|
342
342
|
clauses are excluded. Inspecting rules therefore cannot inspect themselves.
|
|
343
|
+
A rule value used as an inspection pattern is embedded in the inspecting
|
|
344
|
+
statement (Section 4.6), so a stored rule value used as a pattern loses its
|
|
345
|
+
standalone clause and is not in the catalogue.
|
|
343
346
|
|
|
344
347
|
The catalogue keeps source order. A rule's body and forward head conjunctions
|
|
345
348
|
are terms `and(...)`, as normalized goals are, and inspection arguments are
|
|
@@ -1034,7 +1037,7 @@ conclusions and proof documents in the canonical text, and checks every proof
|
|
|
1034
1037
|
it produces as Section 11 describes. A conforming **checker** implements
|
|
1035
1038
|
Sections 9, 11 and 12 and does not depend on a reasoner. For the same
|
|
1036
1039
|
program, a conforming reasoner and checker MUST produce the same
|
|
1037
|
-
conclusions, proof documents and reports as eyemind 0.1.
|
|
1040
|
+
conclusions, proof documents and reports as eyemind 0.1.29, byte for byte,
|
|
1038
1041
|
except for comments and the `Detail` texts of failures (Section 12), the internal names of
|
|
1039
1042
|
variables where Section 3 says so, and where the host's floating-point
|
|
1040
1043
|
library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Backward resolution, forward fixpoints and proof recording.
|
|
2
2
|
import { PRIMITIVE_KEYS, primitive } from './builtins.js';
|
|
3
3
|
import { freshClause, matchHead, renaming, text, variant } from './common.js';
|
|
4
|
-
import {
|
|
4
|
+
import { goalTerm, subgoals } from './goals.js';
|
|
5
5
|
import { INSPECTION_KEYS } from './inspection.js';
|
|
6
6
|
import { checkRendered, clauseDisplay, renderProof } from './proof.js';
|
|
7
7
|
import { CONTROL_KEYS, Program } from './program.js';
|
|
8
8
|
import { isActivation } from './templates.js';
|
|
9
9
|
import {
|
|
10
10
|
Env, EyemindError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
|
|
11
|
-
identity, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems,
|
|
12
|
-
variables,
|
|
11
|
+
identity, isAnonymous, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems,
|
|
12
|
+
unify, variables,
|
|
13
13
|
} from './terms.js';
|
|
14
|
-
import { Lettering, write } from './writer.js';
|
|
14
|
+
import { Lettering, SourceNaming, write } from './writer.js';
|
|
15
15
|
|
|
16
16
|
export const BUILTIN = 'builtin';
|
|
17
17
|
export const CONTROL = 'control';
|
|
@@ -153,6 +153,18 @@ function freshNode(node, suffix) {
|
|
|
153
153
|
return root;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// The error for a negation whose goal still has a variable to bind, naming
|
|
157
|
+
// the variables as the program wrote them rather than as renamed apart.
|
|
158
|
+
function unboundNegation(goal, env) {
|
|
159
|
+
const naming = new SourceNaming();
|
|
160
|
+
const resolved = copyResolved(goal, env);
|
|
161
|
+
const shown = write(resolved, null, naming);
|
|
162
|
+
const unbound = [...new Set([...variables(resolved).values()]
|
|
163
|
+
.filter((variable) => !isAnonymous(variable)).map((variable) => naming.get(variable.name)))];
|
|
164
|
+
return new EyemindError('negation requires a goal whose variables are bound, except for anonymous ones: ' +
|
|
165
|
+
`${unbound.join(', ')} ${unbound.length === 1 ? 'is' : 'are'} unbound in ${shown}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
// Resolve a proof forest with an explicit stack: a derivation chain is as deep
|
|
157
169
|
// as the search that produced it. A derived fact's node is already final and
|
|
158
170
|
// is shared rather than copied, which keeps a long forward chain from being
|
|
@@ -209,13 +221,9 @@ export function clausesBehindBoundaries(program, roots) {
|
|
|
209
221
|
const pending = [];
|
|
210
222
|
function visit(goal) {
|
|
211
223
|
if (!isCallable(goal)) return;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
visit(
|
|
215
|
-
} else if (isTerm(goal, 'not', 1) || isTerm(goal, 'call', 1) || isTerm(goal, 'once', 1)) {
|
|
216
|
-
visit(goal.args[0]);
|
|
217
|
-
} else if (isTerm(goal, 'findall', 3)) {
|
|
218
|
-
visit(goal.args[1]);
|
|
224
|
+
const inner = subgoals(goal);
|
|
225
|
+
if (inner !== null) {
|
|
226
|
+
for (const part of inner) visit(part);
|
|
219
227
|
} else if (!PRIMITIVE_KEYS.has(key(goal))) {
|
|
220
228
|
pending.push(key(goal));
|
|
221
229
|
}
|
|
@@ -235,8 +243,12 @@ export function clausesBehindBoundaries(program, roots) {
|
|
|
235
243
|
const goalKey = pending.pop();
|
|
236
244
|
if (seen.has(goalKey)) continue;
|
|
237
245
|
seen.add(goalKey);
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
if (INSPECTION_KEYS.has(goalKey)) {
|
|
247
|
+
// Inspection reads a rule's structure without solving its body.
|
|
248
|
+
for (const entry of program.inspections.get(goalKey)) reached.add(entry.id);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
for (const clause of byKey.get(goalKey) || []) {
|
|
240
252
|
reached.add(clause.id);
|
|
241
253
|
for (const goal of clause.body) visit(goal);
|
|
242
254
|
}
|
|
@@ -395,10 +407,7 @@ export class Solver {
|
|
|
395
407
|
if (goalKey === 'not/1') {
|
|
396
408
|
// Negation is a test, not a way to bind its variables; its anonymous
|
|
397
409
|
// variables stand for any value.
|
|
398
|
-
if (!isGroundButAnonymous(goal.args[0], env))
|
|
399
|
-
throw new EyemindError('negation requires a goal whose variables are bound, ' +
|
|
400
|
-
`except for anonymous ones: ${text(goal, env)}`);
|
|
401
|
-
}
|
|
410
|
+
if (!isGroundButAnonymous(goal.args[0], env)) throw unboundNegation(goal, env);
|
|
402
411
|
const mark = env.mark();
|
|
403
412
|
const iterator = this.solve([goal.args[0]], env, frame.depth + 1);
|
|
404
413
|
let absent;
|
|
@@ -420,7 +429,7 @@ export class Solver {
|
|
|
420
429
|
this.serial++;
|
|
421
430
|
const names = new Map();
|
|
422
431
|
items.push(freshTerm(copyResolved(goal.args[0], answerEnv), `collection${this.serial}`, names));
|
|
423
|
-
if (this.program.inspectionEnabled) {
|
|
432
|
+
if (this.program.inspectionEnabled && names.size) {
|
|
424
433
|
for (const name of scopedVariables(nodes, answerEnv).keys()) {
|
|
425
434
|
if (names.has(name)) scoped.push(names.get(name));
|
|
426
435
|
}
|
|
@@ -700,24 +709,23 @@ export class Solver {
|
|
|
700
709
|
// between the heads of one conclusion.
|
|
701
710
|
const resolved = copyResolved(head, answerEnv);
|
|
702
711
|
const unresolved = variables(resolved);
|
|
703
|
-
|
|
712
|
+
const captured = clause.captures === null ? null : copyResolved(body[0].args[1], answerEnv);
|
|
713
|
+
const relevant = this.program.inspectionEnabled
|
|
714
|
+
? [...unresolved.keys(), ...(captured === null ? [] : variables(captured).keys())]
|
|
715
|
+
: [];
|
|
716
|
+
// Walking the evidence is linear in its size, so do it only when the
|
|
717
|
+
// conclusion or captures still have variables an inspection could scope.
|
|
718
|
+
if (relevant.length) {
|
|
704
719
|
const scoped = scopedVariables(nodes, answerEnv);
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (relevant.some((name) => scoped.has(name))) {
|
|
708
|
-
const inBody = new Set();
|
|
709
|
-
for (const goal of clause.body) for (const name of variables(goal).keys()) inBody.add(name);
|
|
710
|
-
if ([...variables(clause.head).keys()].some((name) => !inBody.has(name))) {
|
|
711
|
-
throw new EyemindError('bind inspected rule variables before deriving existential witnesses');
|
|
712
|
-
}
|
|
720
|
+
if (clause.witnessNames.length && relevant.some((name) => scoped.has(name))) {
|
|
721
|
+
throw new EyemindError('bind inspected rule variables before deriving existential witnesses');
|
|
713
722
|
}
|
|
714
723
|
for (const name of scoped.keys()) unresolved.delete(name);
|
|
715
724
|
}
|
|
716
725
|
if (unresolved.size) {
|
|
717
726
|
// A generated rule also depends on its captures when its head omits
|
|
718
727
|
// them; otherwise two enabled instances could share a witness.
|
|
719
|
-
const
|
|
720
|
-
const witnesses = skolemTerms(clause, head, unresolved, answerEnv, context);
|
|
728
|
+
const witnesses = skolemTerms(clause, head, unresolved, answerEnv, captured);
|
|
721
729
|
[...unresolved.values()].forEach((value, i) => unify(value, witnesses[i], answerEnv));
|
|
722
730
|
}
|
|
723
731
|
const conclusions = unresolved.size
|
|
@@ -861,10 +869,6 @@ export function run(program, { goal = null, goals = null, proof = false, maxDept
|
|
|
861
869
|
}
|
|
862
870
|
}
|
|
863
871
|
|
|
864
|
-
function goalTerm(goal) {
|
|
865
|
-
return typeof goal === 'string' ? readGoal(goal) : goal;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
872
|
function reason(program, goalInputs, options) {
|
|
869
873
|
const solver = new Solver(program, options);
|
|
870
874
|
const goals = goalInputs.map(goalTerm);
|
package/src/goals.js
CHANGED
|
@@ -9,6 +9,22 @@ import { readTerm } from './reader.js';
|
|
|
9
9
|
|
|
10
10
|
const EMPTY_ENV = new Env();
|
|
11
11
|
|
|
12
|
+
// The calls that state or construct a rule; both their arguments are goal
|
|
13
|
+
// positions.
|
|
14
|
+
export const RULE_NAMES = new Set(['implies', 'implied_by', 'forward', 'backward']);
|
|
15
|
+
|
|
16
|
+
// The goal positions directly inside a control goal, or null when the goal is
|
|
17
|
+
// not a control: both sides of and and or, the argument of not, call and
|
|
18
|
+
// once, and the goal of findall.
|
|
19
|
+
export function subgoals(goal) {
|
|
20
|
+
if (!(goal instanceof Struct)) return null;
|
|
21
|
+
const { name, args } = goal;
|
|
22
|
+
if ((name === 'and' || name === 'or') && args.length === 2) return args;
|
|
23
|
+
if ((name === 'not' || name === 'call' || name === 'once') && args.length === 1) return args;
|
|
24
|
+
if (name === 'findall' && args.length === 3) return [args[1]];
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
function isList(term) {
|
|
13
29
|
return term === EMPTY || (term instanceof Struct && term.name === '.' && term.args.length === 2);
|
|
14
30
|
}
|
|
@@ -22,10 +38,7 @@ export function normalizeGoal(term) {
|
|
|
22
38
|
}
|
|
23
39
|
if (!(term instanceof Struct)) return term;
|
|
24
40
|
const { name, args } = term;
|
|
25
|
-
if ((name === 'and' || name === 'or') && args.length === 2) {
|
|
26
|
-
return rebuild(term, args.map(normalizeGoal));
|
|
27
|
-
}
|
|
28
|
-
if (['implies', 'implied_by', 'forward', 'backward'].includes(name) && args.length === 2) {
|
|
41
|
+
if ((name === 'and' || name === 'or' || RULE_NAMES.has(name)) && args.length === 2) {
|
|
29
42
|
return rebuild(term, args.map(normalizeGoal));
|
|
30
43
|
}
|
|
31
44
|
if ((name === 'not' || name === 'call' || name === 'once') && args.length === 1) {
|
|
@@ -45,3 +58,8 @@ function rebuild(term, args) {
|
|
|
45
58
|
export function readGoal(text) {
|
|
46
59
|
return normalizeGoal(readTerm(text));
|
|
47
60
|
}
|
|
61
|
+
|
|
62
|
+
// A goal given either as text or as a term already.
|
|
63
|
+
export function goalTerm(goal) {
|
|
64
|
+
return typeof goal === 'string' ? readGoal(goal) : goal;
|
|
65
|
+
}
|
package/src/inspection.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Rule inspection reads a fixed source catalogue, not the run's rule index.
|
|
2
|
+
import { subgoals } from './goals.js';
|
|
2
3
|
import { Struct, conjunction, isTerm } from './terms.js';
|
|
3
4
|
|
|
4
5
|
export const INSPECTION_KEYS = new Set(['implies/2', 'implied_by/2']);
|
|
@@ -6,9 +7,8 @@ export const isInspection = (term) => isTerm(term, 'implies', 2) || isTerm(term,
|
|
|
6
7
|
|
|
7
8
|
export function hasInspection(goal) {
|
|
8
9
|
if (isInspection(goal)) return true;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return isTerm(goal, 'findall', 3) && hasInspection(goal.args[1]);
|
|
10
|
+
const inner = subgoals(goal);
|
|
11
|
+
return inner !== null && inner.some(hasInspection);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function inspectionTerm(clause) {
|
package/src/lang.js
CHANGED
|
@@ -277,6 +277,12 @@ const LIBRARY = Object.freeze({
|
|
|
277
277
|
|
|
278
278
|
// The names a program is given: the statements and the terms it builds goals
|
|
279
279
|
// with. The statements report to the innermost program being built.
|
|
280
|
+
// A rule template built by forward() or backward(), which states no clause.
|
|
281
|
+
function template(name, args) {
|
|
282
|
+
if (args.length !== 2) throw new EyemindError(`${name} takes 2 arguments, got ${args.length}`);
|
|
283
|
+
return new Struct(name, args.map(goalOf));
|
|
284
|
+
}
|
|
285
|
+
|
|
280
286
|
function exportsFor(io) {
|
|
281
287
|
const builder = {
|
|
282
288
|
add: (...args) => active().add(...args),
|
|
@@ -288,14 +294,8 @@ function exportsFor(io) {
|
|
|
288
294
|
},
|
|
289
295
|
implies: (...args) => active().rule('implies', args),
|
|
290
296
|
implied_by: (...args) => active().rule('implied_by', args),
|
|
291
|
-
forward: (...args) =>
|
|
292
|
-
|
|
293
|
-
return new Struct('forward', args.map(goalOf));
|
|
294
|
-
},
|
|
295
|
-
backward: (...args) => {
|
|
296
|
-
if (args.length !== 2) throw new EyemindError(`backward takes 2 arguments, got ${args.length}`);
|
|
297
|
-
return new Struct('backward', args.map(goalOf));
|
|
298
|
-
},
|
|
297
|
+
forward: (...args) => template('forward', args),
|
|
298
|
+
backward: (...args) => template('backward', args),
|
|
299
299
|
query: (...body) => builder.add('query', TRUE_ATOM, body.map(goalOf)),
|
|
300
300
|
contradiction: (...body) => builder.add('contradiction', 'false', body.map(goalOf)),
|
|
301
301
|
facts_from: (source) => {
|
package/src/program.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// A program: its clauses, validated, indexed and stratified.
|
|
2
2
|
import { PRIMITIVE_KEYS } from './builtins.js';
|
|
3
3
|
import { ACTIVATION_KEY, containsTemplate, expandTemplates } from './templates.js';
|
|
4
|
+
import { subgoals } from './goals.js';
|
|
4
5
|
import { hasInspection, inspectionTerm } from './inspection.js';
|
|
5
6
|
import {
|
|
6
7
|
Env, EyemindError, Struct, Var, deref, flattenConjunction, freshTerm, identity, isCallable,
|
|
7
|
-
isGround, isTerm, key, keyArity, unify,
|
|
8
|
+
isGround, isTerm, key, keyArity, unify, variables,
|
|
8
9
|
} from './terms.js';
|
|
9
10
|
|
|
10
11
|
export const CONTROL_KEYS = new Set(['and/2', 'or/2', 'not/1', 'call/1', 'once/1', 'findall/3']);
|
|
@@ -26,6 +27,18 @@ export class Clause {
|
|
|
26
27
|
this.captures = captures;
|
|
27
28
|
this.renaming = null; // how to rename it apart, worked out once it is used often
|
|
28
29
|
this.uses = 0;
|
|
30
|
+
this.witnessed = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The names of the variables only the conclusion mentions: a forward rule
|
|
34
|
+
// gives each a Skolem witness per firing (SPEC Section 7.2).
|
|
35
|
+
get witnessNames() {
|
|
36
|
+
if (this.witnessed === null) {
|
|
37
|
+
const inBody = new Set();
|
|
38
|
+
for (const goal of this.body) for (const name of variables(goal).keys()) inBody.add(name);
|
|
39
|
+
this.witnessed = [...variables(this.head).keys()].filter((name) => !inBody.has(name));
|
|
40
|
+
}
|
|
41
|
+
return this.witnessed;
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
|
|
@@ -152,7 +165,7 @@ export class Program {
|
|
|
152
165
|
if (!source || !['forward', 'backward'].includes(source.kind) ||
|
|
153
166
|
containsTemplate(source.head) || source.body.some(hasInspection)) continue;
|
|
154
167
|
const term = inspectionTerm(clause);
|
|
155
|
-
const entry = { id: clause.id, term
|
|
168
|
+
const entry = { id: clause.id, term };
|
|
156
169
|
this.inspectionCatalogue.get(key(term)).push(entry);
|
|
157
170
|
this.inspectionSources.set(clause.id, entry);
|
|
158
171
|
}
|
|
@@ -262,19 +275,12 @@ function bucketEntries(found) {
|
|
|
262
275
|
return found instanceof Bucket ? found.entries : [found];
|
|
263
276
|
}
|
|
264
277
|
|
|
265
|
-
const CONTROL_NAMES = new Set(['and', 'or', 'not', 'call', 'once', 'findall']);
|
|
266
|
-
|
|
267
278
|
// Closed dependencies arise from absence and collection.
|
|
268
279
|
function dependencies(goal, closed = false, out = []) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
dependencies(goal.args[0], true, out);
|
|
274
|
-
} else if (isTerm(goal, 'findall', 3)) {
|
|
275
|
-
dependencies(goal.args[1], true, out);
|
|
276
|
-
} else if (isTerm(goal, 'once', 1) || isTerm(goal, 'call', 1)) {
|
|
277
|
-
dependencies(goal.args[0], closed, out);
|
|
280
|
+
const inner = subgoals(goal);
|
|
281
|
+
if (inner !== null) {
|
|
282
|
+
const inside = closed || isTerm(goal, 'not', 1) || isTerm(goal, 'findall', 3);
|
|
283
|
+
for (const part of inner) dependencies(part, inside, out);
|
|
278
284
|
} else if (goal instanceof Var || (isCallable(goal) && !PRIMITIVE_KEYS.has(key(goal)))) {
|
|
279
285
|
out.push([goal, closed]);
|
|
280
286
|
}
|
|
@@ -283,14 +289,9 @@ function dependencies(goal, closed = false, out = []) {
|
|
|
283
289
|
|
|
284
290
|
// Whether a goal could contribute a closed dependency or a dynamic call.
|
|
285
291
|
function mayNeedStratifying(goal) {
|
|
286
|
-
if (goal instanceof Var) return true;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return mayNeedStratifying(goal.args[0]) || mayNeedStratifying(goal.args[1]);
|
|
290
|
-
}
|
|
291
|
-
if (isTerm(goal, 'not', 1) || isTerm(goal, 'findall', 3)) return true;
|
|
292
|
-
if (isTerm(goal, 'once', 1) || isTerm(goal, 'call', 1)) return mayNeedStratifying(goal.args[0]);
|
|
293
|
-
return false;
|
|
292
|
+
if (goal instanceof Var || isTerm(goal, 'not', 1) || isTerm(goal, 'findall', 3)) return true;
|
|
293
|
+
const inner = subgoals(goal);
|
|
294
|
+
return inner !== null && inner.some(mayNeedStratifying);
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
function headIndex(clauses) {
|
package/src/proof.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// missing derivation. Only pure primitives are independently recomputed.
|
|
11
11
|
import { PRIMITIVE_KEYS, primitive } from './builtins.js';
|
|
12
12
|
import { freshClause, text } from './common.js';
|
|
13
|
-
import {
|
|
13
|
+
import { goalTerm } from './goals.js';
|
|
14
14
|
import { INSPECTION_KEYS, isInspection } from './inspection.js';
|
|
15
15
|
import { CONTROL_KEYS, Program } from './program.js';
|
|
16
16
|
import { readTerms } from './reader.js';
|
|
@@ -66,6 +66,14 @@ function resolvesTo(term, env, target) {
|
|
|
66
66
|
return true;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Whether goal is an instance of pattern: a copy of pattern renamed with
|
|
70
|
+
// suffix matches goal without binding any variable of goal.
|
|
71
|
+
function isInstance(goal, pattern, suffix) {
|
|
72
|
+
const renamed = freshTerm(pattern, suffix);
|
|
73
|
+
const env = new Env();
|
|
74
|
+
return unify(renamed, goal, env) && resolvesTo(renamed, env, goal);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
// The canonical text of terms, written once per term object. A proof names
|
|
70
78
|
// most goals several times, as a step and as a use, and the checks look each
|
|
71
79
|
// up more than once; the text is the same every time.
|
|
@@ -305,13 +313,8 @@ function checkSteps(program, steps, allowTrusted, fail, spellings, name) {
|
|
|
305
313
|
if (steps.has(spellings.spell(goal))) return true;
|
|
306
314
|
// Source facts are available as leaves, including universal facts.
|
|
307
315
|
if (!isCallable(goal)) return false;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const head = freshTerm(clause.head, 'given');
|
|
311
|
-
const env = new Env();
|
|
312
|
-
if (unify(head, goal, env) && resolvesTo(head, env, goal)) return true;
|
|
313
|
-
}
|
|
314
|
-
return false;
|
|
316
|
+
return (program.groups.get(key(goal)) || [])
|
|
317
|
+
.some((clause) => !clause.body.length && isInstance(goal, clause.head, 'given'));
|
|
315
318
|
}
|
|
316
319
|
|
|
317
320
|
for (const step of steps.values()) {
|
|
@@ -324,10 +327,8 @@ function checkSteps(program, steps, allowTrusted, fail, spellings, name) {
|
|
|
324
327
|
} else if (isTerm(by, 'source', 1)) {
|
|
325
328
|
const clause = clauseNamed(program, by.args[0]);
|
|
326
329
|
const inspected = clause === null ? null : program.inspectedById.get(clause.id);
|
|
327
|
-
const env = new Env();
|
|
328
|
-
const pattern = inspected ? freshTerm(inspected.term, 'inspection') : null;
|
|
329
330
|
if (!inspected || bindings.length || uses.length || !isInspection(goal) ||
|
|
330
|
-
!
|
|
331
|
+
!isInstance(goal, inspected.term, 'inspection')) {
|
|
331
332
|
fail.add('C1', `not an instance of inspectable source rule ${text(by.args[0])}: ${text(goal)}`, goal);
|
|
332
333
|
} else tally.verified++;
|
|
333
334
|
} else if (by === BUILTIN_BY) {
|
|
@@ -482,17 +483,6 @@ function checkResolution(program, step, fail, tally) {
|
|
|
482
483
|
return true;
|
|
483
484
|
}
|
|
484
485
|
|
|
485
|
-
// The variables of a forward rule's conclusions that its premise does not
|
|
486
|
-
// mention: each stands for something that exists, its witness.
|
|
487
|
-
function witnessNames(clause) {
|
|
488
|
-
if (clause.witnessNames === undefined) {
|
|
489
|
-
const inBody = new Set();
|
|
490
|
-
for (const goal of clause.body) for (const name of variables(goal).keys()) inBody.add(name);
|
|
491
|
-
clause.witnessNames = [...variables(clause.head).keys()].filter((name) => !inBody.has(name));
|
|
492
|
-
}
|
|
493
|
-
return clause.witnessNames;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
486
|
// C8: under a step's substitution, each witness of a forward rule is the
|
|
497
487
|
// Skolem term of this firing, skolem(Rule, Variable, Arguments), where
|
|
498
488
|
// Arguments are the ground values of the conclusions' other variables, in
|
|
@@ -501,7 +491,7 @@ function witnessNames(clause) {
|
|
|
501
491
|
// rule has no witnesses, else whether they all hold; head is the rule's
|
|
502
492
|
// renamed conclusions and names maps its variables to their renamed ones.
|
|
503
493
|
function checkWitnesses(clause, head, names, env) {
|
|
504
|
-
const witnessed = witnessNames
|
|
494
|
+
const witnessed = clause.witnessNames;
|
|
505
495
|
if (!witnessed.length) return null;
|
|
506
496
|
// An existential firing needs bound inspection values. A generic rule
|
|
507
497
|
// pattern cannot silently share one witness across all its instances.
|
|
@@ -630,24 +620,18 @@ function checkBoundaries(program, steps, boundaries, fail) {
|
|
|
630
620
|
// query() goal or, for printed conclusions, of a forward head.
|
|
631
621
|
function checkRelevance(program, claims, steps, goals, fail, spellings) {
|
|
632
622
|
let serial = 0;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const env = new Env();
|
|
636
|
-
const pattern = freshTerm(question, `asked${serial}`);
|
|
637
|
-
return unify(pattern, claim, env) && resolvesTo(pattern, env, claim);
|
|
638
|
-
}
|
|
639
|
-
const asked = goals.map((goal) => (typeof goal === 'string' ? readGoal(goal) : goal));
|
|
623
|
+
const instanceOf = (claim, question) => isInstance(claim, question, `asked${++serial}`);
|
|
624
|
+
const asked = goals.map(goalTerm);
|
|
640
625
|
const halted = claims.some((claim) => claim === 'false');
|
|
641
626
|
const questions = [];
|
|
642
627
|
if (asked.length && !halted) {
|
|
643
628
|
questions.push(...asked);
|
|
644
629
|
} else {
|
|
645
630
|
for (const clause of program.forward) {
|
|
646
|
-
|
|
647
|
-
if (heads.some((head) => head === 'true')) {
|
|
631
|
+
if (clause.heads.some((head) => head === 'true')) {
|
|
648
632
|
if (!halted && clause.body.length) questions.push(conjunction(clause.body));
|
|
649
633
|
}
|
|
650
|
-
for (const head of heads) if (head !== 'true' && !isActivation(head)) questions.push(head);
|
|
634
|
+
for (const head of clause.heads) if (head !== 'true' && !isActivation(head)) questions.push(head);
|
|
651
635
|
}
|
|
652
636
|
}
|
|
653
637
|
for (const claim of claims) {
|
package/src/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Inline rule templates become statically visible clauses whose first
|
|
2
2
|
// premise records the generating activation. No rule is installed at run time.
|
|
3
|
-
import { normalizeGoal } from './goals.js';
|
|
3
|
+
import { RULE_NAMES, normalizeGoal, subgoals } from './goals.js';
|
|
4
4
|
import { isInspection } from './inspection.js';
|
|
5
5
|
import {
|
|
6
6
|
EMPTY, EyemindError, Struct, Var, conjunction, flattenConjunction, isCallable, isTerm, listFromItems, variables,
|
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export const ACTIVATION_KEY = 'activated/2';
|
|
10
10
|
export const isActivation = (term) => isTerm(term, 'activated', 2);
|
|
11
|
-
const isTemplate = (term) => term instanceof Struct && term.args.length === 2 &&
|
|
12
|
-
['implies', 'implied_by', 'forward', 'backward'].includes(term.name);
|
|
11
|
+
const isTemplate = (term) => term instanceof Struct && term.args.length === 2 && RULE_NAMES.has(term.name);
|
|
13
12
|
|
|
14
13
|
export function containsTemplate(term) {
|
|
15
14
|
const pending = [term];
|
|
@@ -29,15 +28,12 @@ function checkGoal(goal, line, generated = false) {
|
|
|
29
28
|
if (!(goal instanceof Struct)) return;
|
|
30
29
|
if (isInspection(goal)) return;
|
|
31
30
|
if (isTemplate(goal)) throw new EyemindError(`line ${line}: rule templates belong in an implies() conclusion`);
|
|
32
|
-
if (isTerm(goal, '
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw new EyemindError(`line ${line}: rule values in a body must be inspection goals`);
|
|
39
|
-
}
|
|
40
|
-
checkGoal(goal.args[1], line, generated);
|
|
31
|
+
if (isTerm(goal, 'findall', 3) && (containsTemplate(goal.args[0]) || containsTemplate(goal.args[2]))) {
|
|
32
|
+
throw new EyemindError(`line ${line}: rule values in a body must be inspection goals`);
|
|
33
|
+
}
|
|
34
|
+
const inner = subgoals(goal);
|
|
35
|
+
if (inner !== null) {
|
|
36
|
+
for (const part of inner) checkGoal(part, line, generated);
|
|
41
37
|
} else if (containsTemplate(goal)) {
|
|
42
38
|
throw new EyemindError(`line ${line}: rule templates must be top-level conjuncts of an implies() conclusion or inspection goals`);
|
|
43
39
|
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The release this checkout is; ./release keeps it in step with package.json.
|
|
2
|
-
export const VERSION = '0.1.
|
|
2
|
+
export const VERSION = '0.1.29';
|
package/src/writer.js
CHANGED
|
@@ -150,6 +150,34 @@ export class Lettering {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Names for write() that spell variables as the program wrote them, for
|
|
154
|
+
// messages about a running goal: the renamed-apart X#12 is X again and each
|
|
155
|
+
// anonymous variable is _. Distinct variables that share a name are numbered,
|
|
156
|
+
// so a message never shows two of them as one.
|
|
157
|
+
export class SourceNaming {
|
|
158
|
+
constructor() {
|
|
159
|
+
this.names = new Map();
|
|
160
|
+
this.taken = new Set();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get(name) {
|
|
164
|
+
let shown = this.names.get(name);
|
|
165
|
+
if (shown === undefined) {
|
|
166
|
+
const hash = name.indexOf('#');
|
|
167
|
+
const base = hash < 0 ? name : name.slice(0, hash);
|
|
168
|
+
if (/^_[0-9]*$/.test(base)) {
|
|
169
|
+
shown = '_';
|
|
170
|
+
} else {
|
|
171
|
+
shown = variableText(base);
|
|
172
|
+
for (let n = 2; this.taken.has(shown); n++) shown = variableText(`${base}_${n}`);
|
|
173
|
+
this.taken.add(shown);
|
|
174
|
+
}
|
|
175
|
+
this.names.set(name, shown);
|
|
176
|
+
}
|
|
177
|
+
return shown;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
153
181
|
// Names for write() that name nothing but notice a variable.
|
|
154
182
|
class Spy {
|
|
155
183
|
constructor() {
|