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 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.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.30, 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.
@@ -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.
@@ -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, or that name followed
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.30, byte for byte,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyemind",
3
- "version": "0.1.30",
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.30';
2
+ export const VERSION = '0.1.31';