eyemind 0.1.29 → 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 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/)**: 384 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.29
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.29, for readers who want
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.
@@ -272,7 +272,9 @@ name MAY have different ranks.
272
272
  Nested `implies(Body, Head)` and `implied_by(Head, Body)` calls construct rule
273
273
  templates. A rule value in a conclusion MUST occur as a top-level conjunct of
274
274
  an `implies` conclusion; in a body, `implies` and `implied_by` are inspection
275
- goals (Section 4.7). The
275
+ goals (Section 4.7). A rule value anywhere else in a clause, such as an
276
+ argument of an ordinary head or goal, or the template or result of `findall`,
277
+ MUST be rejected. The
276
278
  conclusion MAY combine ordinary heads and several templates. Each template's
277
279
  body and head MAY themselves be conjunctions, written as arrays. A backward
278
280
  template with several heads defines one backward clause per head, sharing
@@ -504,16 +506,20 @@ concluded:
504
506
 
505
507
  - a head variable the solution leaves unbound is bound to a **Skolem term**
506
508
  `skolem(Rule, Variable, Arguments)`: `Rule` is the clause number of the
507
- rule, `Variable` the variable's name as the program writes it (an
508
- 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
509
512
  the ground values the solution gives the heads' other variables, in order
510
513
  of their first occurrence in the heads. A Skolem term is thus a function of
511
514
  the activation: the rule and the instance of its heads. The same activation
512
515
  gives the same terms, in any round and in any run, and different
513
516
  activations give different terms; variables shared between heads stay
514
- shared. A variable that a value of the solution brought in, rather than a
515
- head variable, is named after its own variable, followed by `1`, `2`, ...
516
- 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
517
523
  not terminate (Section 7.3). Derived facts other than scoped inspection
518
524
  patterns (Section 4.7) are therefore ground. A
519
525
  checker verifies the Skolem terms of a proof (C8, Section 11.9);
@@ -576,8 +582,12 @@ An explicit goal MAY ask for an activation. Activations remain available as
576
582
  premises and proof steps; the library's `inferred` list omits them. The
577
583
  `derived` statistic counts them as facts added during reasoning.
578
584
 
579
- The output of a run is the canonical text (Section 8) of each conclusion,
580
- 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).
581
591
 
582
592
  ### 7.7 Proof recording
583
593
 
@@ -683,12 +693,18 @@ string. Reading the canonical text of a term MUST give the term back.
683
693
 
684
694
  A proof document is a document (Section 9) holding, in this order:
685
695
 
686
- 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;
687
698
  2. an empty line;
688
- 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
689
701
  increasing `N`;
690
702
  4. an empty line;
691
- 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.
692
708
 
693
709
  A checker MUST treat every statement that is neither `clause/2` nor `step/4`
694
710
  as a claim, whatever its position.
@@ -836,8 +852,7 @@ value gives an instance of the rule, so for a `clause(N)` step citing a
836
852
  forward rule, under the substitution of C1, each such variable `V` MUST be
837
853
  bound to `skolem(N, Name, Args)`, where:
838
854
 
839
- - `Name` is the name of `V` as the program writes it, or that name followed
840
- by digits (Section 7.2);
855
+ - `Name` is the name of `V` as the program writes it (Section 7.2);
841
856
  - `Args` is the same for every such variable of the step, and is the list of
842
857
  the values of the heads' variables, in order of their first occurrence in
843
858
  the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
@@ -1037,7 +1052,7 @@ conclusions and proof documents in the canonical text, and checks every proof
1037
1052
  it produces as Section 11 describes. A conforming **checker** implements
1038
1053
  Sections 9, 11 and 12 and does not depend on a reasoner. For the same
1039
1054
  program, a conforming reasoner and checker MUST produce the same
1040
- conclusions, proof documents and reports as eyemind 0.1.29, byte for byte,
1055
+ conclusions, proof documents and reports as eyemind 0.1.31, byte for byte,
1041
1056
  except for comments and the `Detail` texts of failures (Section 12), the internal names of
1042
1057
  variables where Section 3 says so, and where the host's floating-point
1043
1058
  library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
@@ -1092,6 +1107,7 @@ query(type(X, Y))
1092
1107
  Its conclusions:
1093
1108
 
1094
1109
  ```js
1110
+ // Answers: each instance of a query() that holds.
1095
1111
  type('socrates', 'mortal')
1096
1112
  type('socrates', 'human')
1097
1113
  ```
@@ -1099,13 +1115,17 @@ type('socrates', 'human')
1099
1115
  Its proof document (Section 10):
1100
1116
 
1101
1117
  ```js
1118
+ // Answers: each instance of a query() that holds.
1102
1119
  type('socrates', 'mortal')
1103
1120
  type('socrates', 'human')
1104
1121
 
1122
+ // Clauses: the program clauses the steps cite, by clause number.
1105
1123
  clause(1, fact(type('socrates', 'human')))
1106
1124
  clause(2, fact(subclass_of('human', 'mortal')))
1107
1125
  clause(3, implies([type(S, A), subclass_of(A, B)], type(S, B)))
1108
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.
1109
1129
  step(type('socrates', 'mortal'), clause(3), {S: 'socrates', A: 'human', B: 'mortal'}, [type('socrates', 'human'), subclass_of('human', 'mortal')])
1110
1130
  step(type('socrates', 'human'), clause(1), {}, [])
1111
1131
  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.29",
3
+ "version": "0.1.31",
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/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
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.29';
2
+ export const VERSION = '0.1.31';