eyemind 0.1.30 → 0.1.31
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 +1 -1
- package/SPEC.md +33 -15
- package/package.json +1 -1
- package/src/engine.js +53 -19
- package/src/program.js +5 -0
- package/src/proof.js +13 -13
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ JavaScript library, and a browser application, with no dependencies.
|
|
|
18
18
|
- **[Examples](https://eyereasoner.github.io/eyemind/examples/)**:
|
|
19
19
|
94 runnable programs, each with a card deck and saved conclusions, proofs
|
|
20
20
|
and check reports.
|
|
21
|
-
- **[Conformance suite](conformance/)**:
|
|
21
|
+
- **[Conformance suite](conformance/)**: 400 cases that test any
|
|
22
22
|
implementation of the specification through its command line.
|
|
23
23
|
|
|
24
24
|
```js
|
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.31
|
|
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.31, 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.
|
|
@@ -506,16 +506,20 @@ concluded:
|
|
|
506
506
|
|
|
507
507
|
- a head variable the solution leaves unbound is bound to a **Skolem term**
|
|
508
508
|
`skolem(Rule, Variable, Arguments)`: `Rule` is the clause number of the
|
|
509
|
-
rule, `Variable` the
|
|
510
|
-
anonymous one as `_0`, `_1`, ..., Section 4.4), and
|
|
509
|
+
rule, `Variable` the name of the head variable the term stands for, as the
|
|
510
|
+
program writes it (an anonymous one as `_0`, `_1`, ..., Section 4.4), and
|
|
511
|
+
`Arguments` the list of
|
|
511
512
|
the ground values the solution gives the heads' other variables, in order
|
|
512
513
|
of their first occurrence in the heads. A Skolem term is thus a function of
|
|
513
514
|
the activation: the rule and the instance of its heads. The same activation
|
|
514
515
|
gives the same terms, in any round and in any run, and different
|
|
515
516
|
activations give different terms; variables shared between heads stay
|
|
516
|
-
shared.
|
|
517
|
-
|
|
518
|
-
|
|
517
|
+
shared. When several head variables stand for one term, the first of them
|
|
518
|
+
in the heads names it, whichever way unification bound them. A variable
|
|
519
|
+
that a value of the solution brought in, which no head variable stands
|
|
520
|
+
for, keeps its own name when no other term of the activation has it; the
|
|
521
|
+
others take that name followed by the first of `1`, `2`, ... that is
|
|
522
|
+
free, in order of first occurrence in the heads. A rule that keeps meeting new activations does
|
|
519
523
|
not terminate (Section 7.3). Derived facts other than scoped inspection
|
|
520
524
|
patterns (Section 4.7) are therefore ground. A
|
|
521
525
|
checker verifies the Skolem terms of a proof (C8, Section 11.9);
|
|
@@ -578,8 +582,12 @@ An explicit goal MAY ask for an activation. Activations remain available as
|
|
|
578
582
|
premises and proof steps; the library's `inferred` list omits them. The
|
|
579
583
|
`derived` statistic counts them as facts added during reasoning.
|
|
580
584
|
|
|
581
|
-
The output of a run is
|
|
582
|
-
one per
|
|
585
|
+
The output of a run is a comment line saying which of these the conclusions
|
|
586
|
+
are, followed by the canonical text (Section 8) of each conclusion, one per
|
|
587
|
+
line. The comment MUST say when the run halted, when the conclusions answer
|
|
588
|
+
goals, when they answer `query()` rules, or when they are derived facts, and
|
|
589
|
+
MUST say so when there are none. Comments are informative and MAY be worded
|
|
590
|
+
differently; they are ignored when the output is read (Section 9).
|
|
583
591
|
|
|
584
592
|
### 7.7 Proof recording
|
|
585
593
|
|
|
@@ -685,12 +693,18 @@ string. Reading the canonical text of a term MUST give the term back.
|
|
|
685
693
|
|
|
686
694
|
A proof document is a document (Section 9) holding, in this order:
|
|
687
695
|
|
|
688
|
-
1. the
|
|
696
|
+
1. the output of the run (Section 7.6): its comment line, then its
|
|
697
|
+
conclusions, one per line;
|
|
689
698
|
2. an empty line;
|
|
690
|
-
3.
|
|
699
|
+
3. when the proof cites any clause, a comment line introducing the clauses,
|
|
700
|
+
then one `clause(N, Display)` per program clause the proof cites, by
|
|
691
701
|
increasing `N`;
|
|
692
702
|
4. an empty line;
|
|
693
|
-
5.
|
|
703
|
+
5. when there are steps, comment lines saying what a step records, then one
|
|
704
|
+
`step(Goal, By, Bindings, Uses)` per justified goal.
|
|
705
|
+
|
|
706
|
+
Comments are informative and MAY be worded differently; they are ignored when
|
|
707
|
+
the document is read.
|
|
694
708
|
|
|
695
709
|
A checker MUST treat every statement that is neither `clause/2` nor `step/4`
|
|
696
710
|
as a claim, whatever its position.
|
|
@@ -838,8 +852,7 @@ value gives an instance of the rule, so for a `clause(N)` step citing a
|
|
|
838
852
|
forward rule, under the substitution of C1, each such variable `V` MUST be
|
|
839
853
|
bound to `skolem(N, Name, Args)`, where:
|
|
840
854
|
|
|
841
|
-
- `Name` is the name of `V` as the program writes it
|
|
842
|
-
by digits (Section 7.2);
|
|
855
|
+
- `Name` is the name of `V` as the program writes it (Section 7.2);
|
|
843
856
|
- `Args` is the same for every such variable of the step, and is the list of
|
|
844
857
|
the values of the heads' variables, in order of their first occurrence in
|
|
845
858
|
the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
|
|
@@ -1039,7 +1052,7 @@ conclusions and proof documents in the canonical text, and checks every proof
|
|
|
1039
1052
|
it produces as Section 11 describes. A conforming **checker** implements
|
|
1040
1053
|
Sections 9, 11 and 12 and does not depend on a reasoner. For the same
|
|
1041
1054
|
program, a conforming reasoner and checker MUST produce the same
|
|
1042
|
-
conclusions, proof documents and reports as eyemind 0.1.
|
|
1055
|
+
conclusions, proof documents and reports as eyemind 0.1.31, byte for byte,
|
|
1043
1056
|
except for comments and the `Detail` texts of failures (Section 12), the internal names of
|
|
1044
1057
|
variables where Section 3 says so, and where the host's floating-point
|
|
1045
1058
|
library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
|
|
@@ -1094,6 +1107,7 @@ query(type(X, Y))
|
|
|
1094
1107
|
Its conclusions:
|
|
1095
1108
|
|
|
1096
1109
|
```js
|
|
1110
|
+
// Answers: each instance of a query() that holds.
|
|
1097
1111
|
type('socrates', 'mortal')
|
|
1098
1112
|
type('socrates', 'human')
|
|
1099
1113
|
```
|
|
@@ -1101,13 +1115,17 @@ type('socrates', 'human')
|
|
|
1101
1115
|
Its proof document (Section 10):
|
|
1102
1116
|
|
|
1103
1117
|
```js
|
|
1118
|
+
// Answers: each instance of a query() that holds.
|
|
1104
1119
|
type('socrates', 'mortal')
|
|
1105
1120
|
type('socrates', 'human')
|
|
1106
1121
|
|
|
1122
|
+
// Clauses: the program clauses the steps cite, by clause number.
|
|
1107
1123
|
clause(1, fact(type('socrates', 'human')))
|
|
1108
1124
|
clause(2, fact(subclass_of('human', 'mortal')))
|
|
1109
1125
|
clause(3, implies([type(S, A), subclass_of(A, B)], type(S, B)))
|
|
1110
1126
|
|
|
1127
|
+
// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
|
|
1128
|
+
// absence or collection taken on trust), the bindings and the goals it uses.
|
|
1111
1129
|
step(type('socrates', 'mortal'), clause(3), {S: 'socrates', A: 'human', B: 'mortal'}, [type('socrates', 'human'), subclass_of('human', 'mortal')])
|
|
1112
1130
|
step(type('socrates', 'human'), clause(1), {}, [])
|
|
1113
1131
|
step(subclass_of('human', 'mortal'), clause(2), {}, [])
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -615,11 +615,9 @@ export class Solver {
|
|
|
615
615
|
directKeys(clause) {
|
|
616
616
|
if (this.direct.has(clause.id)) return this.direct.get(clause.id);
|
|
617
617
|
let keys = [];
|
|
618
|
+
// A forward rule's body goals are all callable: Program rejects numbers,
|
|
619
|
+
// and stratification rejects variables a forward rule can reach.
|
|
618
620
|
for (const goal of clause.body) {
|
|
619
|
-
if (!isCallable(goal)) {
|
|
620
|
-
keys = null;
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
621
|
const goalKey = key(goal);
|
|
624
622
|
if (PRIMITIVE_KEYS.has(goalKey) || INSPECTION_KEYS.has(goalKey) || goalKey === 'not/1' || goalKey === 'findall/3') {
|
|
625
623
|
keys.push(null);
|
|
@@ -808,22 +806,41 @@ export function skolemTerms(clause, head, unresolved, env, context = null) {
|
|
|
808
806
|
if (isGround(value)) values.push(value);
|
|
809
807
|
}
|
|
810
808
|
const argumentsList = listFromItems(values);
|
|
811
|
-
const
|
|
812
|
-
for (const variable of unresolved.values()) {
|
|
813
|
-
// Each variable is named as the program names it; a variable that a value
|
|
814
|
-
// of the solution brought in may share a name, so it gets a number to
|
|
815
|
-
// keep the terms apart.
|
|
809
|
+
const sourceName = (variable) => {
|
|
816
810
|
const hash = variable.name.indexOf('#');
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
811
|
+
return hash < 0 ? variable.name : variable.name.slice(0, hash);
|
|
812
|
+
};
|
|
813
|
+
// A term a head variable stands for is named after that variable, the
|
|
814
|
+
// first in the heads when several share it, whichever way unification
|
|
815
|
+
// bound them. A variable a value of the solution brought in keeps its own
|
|
816
|
+
// name when that is free, and otherwise gets the first free numbered one.
|
|
817
|
+
const named = new Map();
|
|
818
|
+
for (const variable of variables(head).values()) {
|
|
819
|
+
const value = copyResolved(variable, env);
|
|
820
|
+
if (value instanceof Var && unresolved.has(value.name) && !named.has(value.name)) {
|
|
821
|
+
named.set(value.name, sourceName(variable));
|
|
823
822
|
}
|
|
824
|
-
names.push(name);
|
|
825
823
|
}
|
|
826
|
-
|
|
824
|
+
const taken = new Set(named.values());
|
|
825
|
+
const clashing = [];
|
|
826
|
+
for (const variable of unresolved.values()) {
|
|
827
|
+
if (named.has(variable.name)) continue;
|
|
828
|
+
const base = sourceName(variable);
|
|
829
|
+
if (taken.has(base)) {
|
|
830
|
+
clashing.push(variable);
|
|
831
|
+
} else {
|
|
832
|
+
taken.add(base);
|
|
833
|
+
named.set(variable.name, base);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
for (const variable of clashing) {
|
|
837
|
+
const base = sourceName(variable);
|
|
838
|
+
let name = base;
|
|
839
|
+
for (let number = 1; taken.has(name); number++) name = `${base}${number}`;
|
|
840
|
+
taken.add(name);
|
|
841
|
+
named.set(variable.name, name);
|
|
842
|
+
}
|
|
843
|
+
return [...unresolved.keys()].map((name) => new Struct('skolem', [BigInt(clause.id), named.get(name), argumentsList]));
|
|
827
844
|
}
|
|
828
845
|
|
|
829
846
|
// What a run concluded. answers are the conclusions as printable text and
|
|
@@ -869,6 +886,20 @@ export function run(program, { goal = null, goals = null, proof = false, maxDept
|
|
|
869
886
|
}
|
|
870
887
|
}
|
|
871
888
|
|
|
889
|
+
// The comment that opens a run's output and proof: what its conclusions are
|
|
890
|
+
// (SPEC Section 7.6), and that there are none when there are none.
|
|
891
|
+
function conclusionsHeading(kind, count) {
|
|
892
|
+
if (kind === 'halted') return "// Halted: a contradiction() holds; the facts derived until then, ending with 'false'.";
|
|
893
|
+
if (kind === 'goals') {
|
|
894
|
+
return count ? '// Answers: each instance of a goal that holds.' : '// No answers: no instance of a goal holds.';
|
|
895
|
+
}
|
|
896
|
+
if (kind === 'queries') {
|
|
897
|
+
return count ? '// Answers: each instance of a query() that holds.' : '// No answers: no query() instance holds.';
|
|
898
|
+
}
|
|
899
|
+
return count ? '// Conclusions: the facts the program derives, in the order derived.'
|
|
900
|
+
: '// No conclusions: the program derives no facts.';
|
|
901
|
+
}
|
|
902
|
+
|
|
872
903
|
function reason(program, goalInputs, options) {
|
|
873
904
|
const solver = new Solver(program, options);
|
|
874
905
|
const goals = goalInputs.map(goalTerm);
|
|
@@ -906,6 +937,9 @@ function reason(program, goalInputs, options) {
|
|
|
906
937
|
claims.push(node.goal);
|
|
907
938
|
}
|
|
908
939
|
}
|
|
940
|
+
const heading = conclusionsHeading(solver.haltCode !== null ? 'halted'
|
|
941
|
+
: goals.length ? 'goals'
|
|
942
|
+
: program.forward.some((clause) => clause.heads.includes('true')) ? 'queries' : 'derived', claims.length);
|
|
909
943
|
// Variables are named A, B, ... in the order the conclusions show them; the
|
|
910
944
|
// bindings of each answer name them as its conclusion does.
|
|
911
945
|
const lettering = new Lettering();
|
|
@@ -915,7 +949,7 @@ function reason(program, goalInputs, options) {
|
|
|
915
949
|
for (const [name, value] of answer) out[name] = write(value, null, lettering);
|
|
916
950
|
return out;
|
|
917
951
|
});
|
|
918
|
-
const rendered = options.proof ? renderProof(program, claims, roots, true) : null;
|
|
952
|
+
const rendered = options.proof ? renderProof(program, claims, roots, true, heading) : null;
|
|
919
953
|
const document = rendered !== null ? rendered[0] : null;
|
|
920
954
|
// A rule that reports a claim is used by it, though a proof records only
|
|
921
955
|
// the claim's support.
|
|
@@ -939,7 +973,7 @@ function reason(program, goalInputs, options) {
|
|
|
939
973
|
derived: solver.derived,
|
|
940
974
|
clausesUsed: used !== null ? [...used].sort((a, b) => a - b) : null,
|
|
941
975
|
clausesBehindBoundaries: behind,
|
|
942
|
-
stdout: document !== null ? document : answers.map((
|
|
976
|
+
stdout: document !== null ? document : [heading, ...answers].map((line) => `${line}\n`).join(''),
|
|
943
977
|
proof: document,
|
|
944
978
|
proofReport: report,
|
|
945
979
|
stats: solver.stats,
|
package/src/program.js
CHANGED
|
@@ -114,6 +114,11 @@ export class Program {
|
|
|
114
114
|
'write query(Premise) or contradiction(Premise)');
|
|
115
115
|
}
|
|
116
116
|
for (const goal of body) {
|
|
117
|
+
// A statement checks this as it runs; a program built from its
|
|
118
|
+
// clauses directly is checked here, so no search meets a number.
|
|
119
|
+
if (!isCallable(goal) && !(goal instanceof Var)) {
|
|
120
|
+
throw new EyemindError(`line ${source.line}: a goal must be an atom, a compound term or a variable, not a number`);
|
|
121
|
+
}
|
|
117
122
|
if (!stratifying && mayNeedStratifying(goal)) stratifying = true;
|
|
118
123
|
}
|
|
119
124
|
let headKey = null;
|
package/src/proof.js
CHANGED
|
@@ -116,10 +116,11 @@ function bindingsText(bindings, lettering) {
|
|
|
116
116
|
return `{${parts.join(', ')}}`;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// The proof document of a run's claims and their derivations
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
119
|
+
// The proof document of a run's claims and their derivations, opened by
|
|
120
|
+
// heading, the comment that says what the claims are. With checkable, also
|
|
121
|
+
// what checkRendered() needs to check it without reading the document back:
|
|
122
|
+
// the document and that, as a pair. Comments are ignored when it is read.
|
|
123
|
+
export function renderProof(program, claims, roots, checkable = false, heading = null) {
|
|
123
124
|
const spellings = new Spellings();
|
|
124
125
|
const steps = new Map();
|
|
125
126
|
const cited = new Set();
|
|
@@ -135,12 +136,18 @@ export function renderProof(program, claims, roots, checkable = false) {
|
|
|
135
136
|
// Variables are named A, B, ... in the order the document first shows
|
|
136
137
|
// them, the claims written first, as in the run's conclusions.
|
|
137
138
|
const lettering = new Lettering();
|
|
138
|
-
const lines =
|
|
139
|
+
const lines = heading === null ? [] : [heading];
|
|
140
|
+
for (const claim of claims) lines.push(spellings.lettered(claim, lettering));
|
|
139
141
|
lines.push('');
|
|
142
|
+
if (cited.size) lines.push('// Clauses: the program clauses the steps cite, by clause number.');
|
|
140
143
|
for (const clauseId of [...cited].sort((a, b) => a - b)) {
|
|
141
144
|
lines.push(text(new Struct('clause', [BigInt(clauseId), clauseDisplay(program.byId.get(clauseId))])));
|
|
142
145
|
}
|
|
143
146
|
lines.push('');
|
|
147
|
+
if (steps.size) {
|
|
148
|
+
lines.push('// Steps: each goal, what justifies it (a clause, a builtin, a control, or an',
|
|
149
|
+
'// absence or collection taken on trust), the bindings and the goals it uses.');
|
|
150
|
+
}
|
|
144
151
|
for (const node of steps.values()) {
|
|
145
152
|
const uses = `[${node.children.map((child) => spellings.lettered(child.goal, lettering)).join(', ')}]`;
|
|
146
153
|
lines.push(`step(${spellings.lettered(node.goal, lettering)}, ${text(node.by)}, ` +
|
|
@@ -499,7 +506,7 @@ function checkWitnesses(clause, head, names, env) {
|
|
|
499
506
|
let firing = null;
|
|
500
507
|
for (const name of witnessed) {
|
|
501
508
|
const value = copyResolved(names.get(name), env);
|
|
502
|
-
if (!isTerm(value, 'skolem', 3) || value.args[0] !== BigInt(clause.id) ||
|
|
509
|
+
if (!isTerm(value, 'skolem', 3) || value.args[0] !== BigInt(clause.id) || value.args[1] !== name) return false;
|
|
503
510
|
if (firing === null) firing = value.args[2];
|
|
504
511
|
else if (text(value.args[2]) !== text(firing)) return false;
|
|
505
512
|
}
|
|
@@ -526,13 +533,6 @@ function checkWitnesses(clause, head, names, env) {
|
|
|
526
533
|
return text(listFromItems(expected)) === text(firing);
|
|
527
534
|
}
|
|
528
535
|
|
|
529
|
-
// The name a witness of variable name carries: the name itself, or with a
|
|
530
|
-
// number when a value of the firing brought in a variable of the same name.
|
|
531
|
-
function witnessName(written, name) {
|
|
532
|
-
if (typeof written !== 'string' || !written.startsWith(name)) return false;
|
|
533
|
-
return written === name || /^[0-9]+$/.test(written.slice(name.length));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
536
|
// C6: a trusted boundary cannot be proved, but evidence at hand can refute
|
|
537
537
|
// it. An absence fails when a source fact, a step of this certificate or a
|
|
538
538
|
// recomputed primitive is a solution; a collection fails when such a solution
|
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.31';
|