eyemind 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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/)**: 398 cases that test any
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.30
5
+ Version: eyemind 0.1.32
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.30, for readers who want
25
+ the language and formats as implemented by eyemind 0.1.32, 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 variable's name as the program writes it (an
510
- anonymous one as `_0`, `_1`, ..., Section 4.4), and `Arguments` the list of
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. A variable that a value of the solution brought in, rather than a
517
- head variable, is named after its own variable, followed by `1`, `2`, ...
518
- when that name is taken. A rule that keeps meeting new activations does
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 the canonical text (Section 8) of each conclusion,
582
- one per line.
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 conclusions of the run (Section 7.6), one per line;
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. one `clause(N, Display)` per program clause the proof cites, by
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. one `step(Goal, By, Bindings, Uses)` per justified goal.
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.
@@ -806,9 +820,12 @@ forbidden ("strict"), each `'absent'` or `'collected'` step fails C5.
806
820
 
807
821
  A trusted boundary cannot be proved, but the evidence at hand can refute it.
808
822
  The evidence for a goal is: when the goal is a ground primitive, the goal
809
- itself if it holds; when it is a control, none, and the boundary is left
810
- undecided; otherwise the program's facts and the goals of the document's
811
- steps with the same name and arity.
823
+ itself if it holds, and otherwise the recomputed fact that it fails; when it
824
+ is a control, none, and the boundary is left undecided; otherwise the
825
+ program's facts and the goals of the document's steps with the same name and
826
+ arity. A goal of the last kind **has evidence** only when there is at least
827
+ one such fact or step goal; with none, nothing could refute the boundary, and
828
+ it is left undecided.
812
829
 
813
830
  - An absence `not(G)` is decided when `G` is a single goal, or a ground
814
831
  conjunction, whose every goal has evidence. It fails C6 when every goal of
@@ -838,8 +855,7 @@ value gives an instance of the rule, so for a `clause(N)` step citing a
838
855
  forward rule, under the substitution of C1, each such variable `V` MUST be
839
856
  bound to `skolem(N, Name, Args)`, where:
840
857
 
841
- - `Name` is the name of `V` as the program writes it, or that name followed
842
- by digits (Section 7.2);
858
+ - `Name` is the name of `V` as the program writes it (Section 7.2);
843
859
  - `Args` is the same for every such variable of the step, and is the list of
844
860
  the values of the heads' variables, in order of their first occurrence in
845
861
  the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
@@ -1039,7 +1055,7 @@ conclusions and proof documents in the canonical text, and checks every proof
1039
1055
  it produces as Section 11 describes. A conforming **checker** implements
1040
1056
  Sections 9, 11 and 12 and does not depend on a reasoner. For the same
1041
1057
  program, a conforming reasoner and checker MUST produce the same
1042
- conclusions, proof documents and reports as eyemind 0.1.30, byte for byte,
1058
+ conclusions, proof documents and reports as eyemind 0.1.32, byte for byte,
1043
1059
  except for comments and the `Detail` texts of failures (Section 12), the internal names of
1044
1060
  variables where Section 3 says so, and where the host's floating-point
1045
1061
  library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
@@ -1094,6 +1110,7 @@ query(type(X, Y))
1094
1110
  Its conclusions:
1095
1111
 
1096
1112
  ```js
1113
+ // Answers: each instance of a query() that holds.
1097
1114
  type('socrates', 'mortal')
1098
1115
  type('socrates', 'human')
1099
1116
  ```
@@ -1101,13 +1118,17 @@ type('socrates', 'human')
1101
1118
  Its proof document (Section 10):
1102
1119
 
1103
1120
  ```js
1121
+ // Answers: each instance of a query() that holds.
1104
1122
  type('socrates', 'mortal')
1105
1123
  type('socrates', 'human')
1106
1124
 
1125
+ // Clauses: the program clauses the steps cite, by clause number.
1107
1126
  clause(1, fact(type('socrates', 'human')))
1108
1127
  clause(2, fact(subclass_of('human', 'mortal')))
1109
1128
  clause(3, implies([type(S, A), subclass_of(A, B)], type(S, B)))
1110
1129
 
1130
+ // Steps: each goal, what justifies it (a clause, a builtin, a control, or an
1131
+ // absence or collection taken on trust), the bindings and the goals it uses.
1111
1132
  step(type('socrates', 'mortal'), clause(3), {S: 'socrates', A: 'human', B: 'mortal'}, [type('socrates', 'human'), subclass_of('human', 'mortal')])
1112
1133
  step(type('socrates', 'human'), clause(1), {}, [])
1113
1134
  step(subclass_of('human', 'mortal'), clause(2), {}, [])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyemind",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "A dependency-free JavaScript rule language with forward and backward reasoning and checkable proofs.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/cli.js CHANGED
@@ -74,6 +74,12 @@ export function main(argv, io) {
74
74
  i++;
75
75
  if (i >= argv.length) throw new UsageError(`${arg} needs a value`);
76
76
  const value = argv[i];
77
+ // An option in the value's place means the value was left out, as in
78
+ // --check-proof --strict-proof proof.js; reading the option as a file
79
+ // name would only fail later with a puzzling message.
80
+ if (/^--./.test(value) || ['-p', '-c', '-h'].includes(value)) {
81
+ throw new UsageError(`${arg} needs a value right after it, not the option ${value}`);
82
+ }
77
83
  if (arg === '--check-proof') {
78
84
  proofFile = value;
79
85
  } else if (arg === '--goal') {
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 names = [];
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
- const base = hash < 0 ? variable.name : variable.name.slice(0, hash);
818
- let name = base;
819
- let number = 1;
820
- while (names.includes(name)) {
821
- name = `${base}${number}`;
822
- number++;
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
- return names.map((name) => new Struct('skolem', [BigInt(clause.id), name, argumentsList]));
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((answer) => `${answer}\n`).join(''),
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. With
120
- // checkable, also what checkRendered() needs to check it without reading the
121
- // document back: the document and that, as a pair.
122
- export function renderProof(program, claims, roots, checkable = false) {
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 = claims.map((claim) => spellings.lettered(claim, lettering));
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) || !witnessName(value.args[1], name)) return false;
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
@@ -552,7 +552,9 @@ function checkBoundaries(program, steps, boundaries, fail) {
552
552
  const nextSerial = () => ++serial;
553
553
 
554
554
  // Each solution of a simple goal that the evidence shows, or null when the
555
- // evidence cannot speak for the goal.
555
+ // evidence cannot speak for the goal. A recomputed primitive always speaks,
556
+ // even when it fails; a predicate with no fact or step at all does not, so
557
+ // a boundary over it is left undecided rather than counted as confronted.
556
558
  function evidence(goal) {
557
559
  if (!isCallable(goal)) return null;
558
560
  const goalKey = key(goal);
@@ -567,13 +569,16 @@ function checkBoundaries(program, steps, boundaries, fail) {
567
569
  return [];
568
570
  }
569
571
  if (CONTROL_KEYS.has(goalKey)) return null;
572
+ let shown;
570
573
  if (INSPECTION_KEYS.has(goalKey)) {
571
- return program.inspections.get(goalKey).map((entry) => freshTerm(entry.term, `evidence${nextSerial()}`));
574
+ shown = program.inspections.get(goalKey).map((entry) => freshTerm(entry.term, `evidence${nextSerial()}`));
575
+ } else {
576
+ const facts = (program.groups.get(goalKey) || [])
577
+ .filter((clause) => !clause.body.length && !clause.forward)
578
+ .map((clause) => freshTerm(clause.head, `evidence${nextSerial()}`));
579
+ shown = [...facts, ...(stepGoals.get(goalKey) || []).map((item) => freshTerm(item, `evidence${nextSerial()}`))];
572
580
  }
573
- const facts = (program.groups.get(goalKey) || [])
574
- .filter((clause) => !clause.body.length && !clause.forward)
575
- .map((clause) => freshTerm(clause.head, `evidence${nextSerial()}`));
576
- return [...facts, ...(stepGoals.get(goalKey) || []).map((item) => freshTerm(item, `evidence${nextSerial()}`))];
581
+ return shown.length ? shown : null;
577
582
  }
578
583
 
579
584
  for (const boundary of boundaries) {
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.30';
2
+ export const VERSION = '0.1.32';