eyemind 0.1.28 → 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 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.28
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.28, for readers who want
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.
@@ -1037,7 +1037,7 @@ conclusions and proof documents in the canonical text, and checks every proof
1037
1037
  it produces as Section 11 describes. A conforming **checker** implements
1038
1038
  Sections 9, 11 and 12 and does not depend on a reasoner. For the same
1039
1039
  program, a conforming reasoner and checker MUST produce the same
1040
- conclusions, proof documents and reports as eyemind 0.1.28, byte for byte,
1040
+ conclusions, proof documents and reports as eyemind 0.1.29, byte for byte,
1041
1041
  except for comments and the `Detail` texts of failures (Section 12), the internal names of
1042
1042
  variables where Section 3 says so, and where the host's floating-point
1043
1043
  library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyemind",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
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
@@ -1,7 +1,7 @@
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 { readGoal } from './goals.js';
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';
@@ -221,13 +221,9 @@ export function clausesBehindBoundaries(program, roots) {
221
221
  const pending = [];
222
222
  function visit(goal) {
223
223
  if (!isCallable(goal)) return;
224
- if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) {
225
- visit(goal.args[0]);
226
- visit(goal.args[1]);
227
- } else if (isTerm(goal, 'not', 1) || isTerm(goal, 'call', 1) || isTerm(goal, 'once', 1)) {
228
- visit(goal.args[0]);
229
- } else if (isTerm(goal, 'findall', 3)) {
230
- visit(goal.args[1]);
224
+ const inner = subgoals(goal);
225
+ if (inner !== null) {
226
+ for (const part of inner) visit(part);
231
227
  } else if (!PRIMITIVE_KEYS.has(key(goal))) {
232
228
  pending.push(key(goal));
233
229
  }
@@ -247,8 +243,12 @@ export function clausesBehindBoundaries(program, roots) {
247
243
  const goalKey = pending.pop();
248
244
  if (seen.has(goalKey)) continue;
249
245
  seen.add(goalKey);
250
- const candidates = INSPECTION_KEYS.has(goalKey) ? program.inspections.get(goalKey) : byKey.get(goalKey) || [];
251
- for (const clause of candidates) {
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) || []) {
252
252
  reached.add(clause.id);
253
253
  for (const goal of clause.body) visit(goal);
254
254
  }
@@ -717,12 +717,8 @@ export class Solver {
717
717
  // conclusion or captures still have variables an inspection could scope.
718
718
  if (relevant.length) {
719
719
  const scoped = scopedVariables(nodes, answerEnv);
720
- if (relevant.some((name) => scoped.has(name))) {
721
- const inBody = new Set();
722
- for (const goal of clause.body) for (const name of variables(goal).keys()) inBody.add(name);
723
- if ([...variables(clause.head).keys()].some((name) => !inBody.has(name))) {
724
- throw new EyemindError('bind inspected rule variables before deriving existential witnesses');
725
- }
720
+ if (clause.witnessNames.length && relevant.some((name) => scoped.has(name))) {
721
+ throw new EyemindError('bind inspected rule variables before deriving existential witnesses');
726
722
  }
727
723
  for (const name of scoped.keys()) unresolved.delete(name);
728
724
  }
@@ -873,10 +869,6 @@ export function run(program, { goal = null, goals = null, proof = false, maxDept
873
869
  }
874
870
  }
875
871
 
876
- function goalTerm(goal) {
877
- return typeof goal === 'string' ? readGoal(goal) : goal;
878
- }
879
-
880
872
  function reason(program, goalInputs, options) {
881
873
  const solver = new Solver(program, options);
882
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
- if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) return goal.args.some(hasInspection);
10
- if (isTerm(goal, 'not', 1) || isTerm(goal, 'call', 1) || isTerm(goal, 'once', 1)) return hasInspection(goal.args[0]);
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
- if (args.length !== 2) throw new EyemindError(`forward takes 2 arguments, got ${args.length}`);
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, body: [] };
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
- if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) {
270
- dependencies(goal.args[0], closed, out);
271
- dependencies(goal.args[1], closed, out);
272
- } else if (isTerm(goal, 'not', 1)) {
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
- if (!(goal instanceof Struct) || !CONTROL_NAMES.has(goal.name)) return false;
288
- if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) {
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 { readGoal } from './goals.js';
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
- for (const clause of program.groups.get(key(goal)) || []) {
309
- if (clause.body.length) continue;
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
- !unify(pattern, goal, env) || !resolvesTo(pattern, env, goal)) {
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(clause);
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
- function instanceOf(claim, question) {
634
- serial++;
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
- const heads = flattenConjunction(clause.head);
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, 'and', 2) || isTerm(goal, 'or', 2)) {
33
- for (const part of goal.args) checkGoal(part, line, generated);
34
- } else if (['not', 'call', 'once'].includes(goal.name) && goal.args.length === 1) {
35
- checkGoal(goal.args[0], line, generated);
36
- } else if (isTerm(goal, 'findall', 3)) {
37
- if (containsTemplate(goal.args[0]) || containsTemplate(goal.args[2])) {
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.28';
2
+ export const VERSION = '0.1.29';