@josd/eyecode 0.1.5 → 0.1.6

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
@@ -14,7 +14,7 @@ JavaScript library, and a browser application, with no dependencies.
14
14
  - **[Guide](https://eyereasoner.github.io/eyecode/GUIDE)**: run programs,
15
15
  write rules, check proofs, use the library, and develop eyecode.
16
16
  - **[Specification](https://eyereasoner.github.io/eyecode/SPEC)**: the
17
- language, its canonical text, the proof format, and the C1–C7 checks.
17
+ language, its canonical text, the proof format, and the C1–C8 checks.
18
18
  - **[Examples](examples/)**: 63 runnable programs, with their conclusions,
19
19
  proofs and check reports, and a
20
20
  [card deck](https://eyereasoner.github.io/eyecode/examples/deck/) for each.
package/SPEC.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ```text
4
4
  Title: The eyecode Rule Language and Proof Format
5
- Version: eyecode 0.1.5
5
+ Version: eyecode 0.1.6
6
6
  Status: Informational
7
7
  Author: Jos De Roo, KNoWS office of IDLab, Ghent University - imec
8
8
  Repository: https://github.com/eyereasoner/eyecode
@@ -15,14 +15,14 @@ reasoning, and a proof format whose documents a separate checker verifies
15
15
  against the program that produced them. This document specifies the terms
16
16
  the language reasons over, how a program is stated in JavaScript, what a
17
17
  program concludes, the canonical text in which terms, conclusions, proofs and
18
- check reports are written and read back, the proof document, the seven
19
- conditions (C1-C7) a proof checker establishes, the check report, and the
18
+ check reports are written and read back, the proof document, the eight
19
+ conditions (C1-C8) a proof checker establishes, the check report, and the
20
20
  command line.
21
21
 
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 eyecode 0.1.5, for readers who want
25
+ the language and formats as implemented by eyecode 0.1.6, 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.
@@ -393,7 +393,8 @@ concluded:
393
393
  shared. A variable that a value of the solution brought in, rather than a
394
394
  head variable, is named after its own variable, followed by `1`, `2`, ...
395
395
  when that name is taken. A rule that keeps meeting new activations does
396
- not terminate (Section 7.3). Derived facts are therefore ground;
396
+ not terminate (Section 7.3). Derived facts are therefore ground. A
397
+ checker verifies the Skolem terms of a proof (C8, Section 11.9);
397
398
  - a head `'true'` reports the instance of the rule's body, as a conjunction,
398
399
  once per distinct instance (by identity of canonical text);
399
400
  - a head `'false'` records the conclusion `'false'` and stops all reasoning
@@ -593,7 +594,7 @@ empty bindings.
593
594
  A checker is given a program, a proof document and, when the proof answers
594
595
  goals asked from outside the program, those goals. It MUST NOT call a
595
596
  reasoner to supply a missing step; it MAY recompute primitives. It
596
- establishes seven conditions and records each failure with its condition, a
597
+ establishes eight conditions and records each failure with its condition, a
597
598
  detail and, where there is one, the term concerned.
598
599
 
599
600
  ### 11.1 Reading the document
@@ -669,7 +670,28 @@ Otherwise the questions are, for each forward rule, each head other than
669
670
  question. Each step MUST be reachable from a conjunct of a claim through the
670
671
  conjuncts of uses.
671
672
 
672
- ### 11.9 Counts and validity
673
+ ### 11.9 C8 Witness integrity
674
+
675
+ A forward rule's head MAY contain variables its body does not contain; each
676
+ says that something exists, and a reasoner concludes it with a Skolem term
677
+ (Section 7.2). C1 alone would accept any value for such a variable, since any
678
+ value gives an instance of the rule, so for a `clause(N)` step citing a
679
+ forward rule, under the substitution of C1, each such variable `V` MUST be
680
+ bound to `skolem(N, Name, Args)`, where:
681
+
682
+ - `Name` is the name of `V` as the program writes it, or that name followed
683
+ by digits (Section 7.2);
684
+ - `Args` is the same for every such variable of the step, and is the list of
685
+ the values of the heads' variables, in order of their first occurrence in
686
+ the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
687
+ the values that do contain one hold a witness of this firing.
688
+
689
+ A variable of the head that also occurs in the body is not checked by C8,
690
+ even when the body leaves it unbound; its value comes from the uses, which
691
+ other conditions check. A step citing a rule without such variables is not
692
+ covered by C8.
693
+
694
+ ### 11.10 Counts and validity
673
695
 
674
696
  | Condition | Covered |
675
697
  | --- | --- |
@@ -680,6 +702,7 @@ conjuncts of uses.
680
702
  | C5 re_decision | `'builtin'` and `'control'` steps that passed C5 |
681
703
  | C6 boundary_consistency | boundaries decided |
682
704
  | C7 relevance | claims plus steps |
705
+ | C8 witness_integrity | `clause` steps of forward rules with witnesses that passed C8 |
683
706
 
684
707
  A check also counts the **steps** it read, the **claims**, the `clause` steps
685
708
  **verified** by C1, the `'builtin'` steps **recomputed** by C5, the
@@ -689,7 +712,7 @@ steps **trusted**.
689
712
  A proof is **valid** when no condition failed. A valid proof with trusted
690
713
  boundaries is valid *conditional on* them.
691
714
 
692
- ### 11.10 What a valid proof establishes
715
+ ### 11.11 What a valid proof establishes
693
716
 
694
717
  A valid proof establishes that each of its claims follows from the program
695
718
  by the recorded steps, given its trusted boundaries. It does not establish
@@ -702,23 +725,23 @@ leave conclusions out, and a run's own proof holds exactly its conclusions.
702
725
  A check report is a document (Section 9) holding, in this order, one fact per
703
726
  line:
704
727
 
705
- 1. `condition(Id, Name, Outcome, Covered)` for C1 to C7, where `Outcome` is
728
+ 1. `condition(Id, Name, Outcome, Covered)` for C1 to C8, where `Outcome` is
706
729
  `'ok'` or `failed(N)`, `N` the number of that condition's failures;
707
730
  2. `failure(Condition, Subject, Detail)` per failure, where `Subject` is the
708
731
  term concerned or `'proof_document'`, and `Detail` an atom;
709
732
  3. `obligation(Kind, 'theory_scoped', Goal)` per trusted boundary, in step
710
733
  order, where `Kind` is `'absent'` or `'collected'`;
711
734
  4. `steps(N)`, `verified(N)`, `recomputed(N)`, `composed(N)`, `trusted(N)`,
712
- `claims(N)`, the counts of Section 11.9;
735
+ `claims(N)`, the counts of Section 11.10;
713
736
  5. `verdict(V)`, where `V` is `failed(N)` when `N` failures were recorded,
714
737
  otherwise `'checked_with_obligations'` when there are obligations,
715
738
  otherwise `'checked'`.
716
739
 
717
740
  Failures are listed in the order they are found: those of reading the
718
741
  document (Section 11.1), in document order; C4 for each claim, in claim
719
- order; then for each step, in document order, C4 for its uses and the C1, C3
720
- or C5 failure of its justification; C6 for each boundary, in step order; C7
721
- for each claim, then for each step; and C2.
742
+ order; then for each step, in document order, C4 for its uses and the C1, C3,
743
+ C5 or C8 failure of its justification; C6 for each boundary, in step order;
744
+ C7 for each claim, then for each step; and C2.
722
745
 
723
746
  `Detail` is a short human-readable text that names what failed, often with
724
747
  the canonical text of a term; it is informative, and implementations MAY word
@@ -733,6 +756,7 @@ it differently. eyecode's details are, by condition:
733
756
  | C5 | `primitive disagrees: ...`, `control step does not follow from its uses: ...`, `trusted boundary forbidden: absent` or `collected` |
734
757
  | C6 | `absence contradicted by evidence: ...`, `collected result is not a proper list: ...`, `collection misses ...: ...` |
735
758
  | C7 | `claim answers no goal: ...`, `step serves no claim: ...` |
759
+ | C8 | `not the witness of this firing of clause N: ...` |
736
760
 
737
761
  The variables of each fact are renamed `A`, `B`, ..., `Z`, `A1`, ... in order
738
762
  of first occurrence. A report is ordinary data: `facts_from` (Section 4.2)
@@ -816,7 +840,7 @@ proofs SHOULD additionally bound document size, nesting depth and running
816
840
  time.
817
841
 
818
842
  A valid proof establishes that its claims follow from the program supplied
819
- to the checker (Section 11.10). It does not establish that the program is
843
+ to the checker (Section 11.11). It does not establish that the program is
820
844
  correct or that the claims are all its conclusions, and a proof with
821
845
  obligations holds only conditional on them.
822
846
 
@@ -827,7 +851,7 @@ conclusions and proof documents in the canonical text, and checks every proof
827
851
  it produces as Section 11 describes. A conforming **checker** implements
828
852
  Sections 9, 11 and 12 and does not depend on a reasoner. For the same
829
853
  program, a conforming reasoner and checker MUST produce the same
830
- conclusions, proof documents and reports as eyecode 0.1.5, byte for byte,
854
+ conclusions, proof documents and reports as eyecode 0.1.6, byte for byte,
831
855
  except for the `Detail` texts of failures (Section 12), the internal names of
832
856
  variables where Section 3 says so, and where the host's floating-point
833
857
  library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
@@ -911,6 +935,7 @@ condition('C4', 'coverage', 'ok', 4)
911
935
  condition('C5', 're_decision', 'ok', 0)
912
936
  condition('C6', 'boundary_consistency', 'ok', 0)
913
937
  condition('C7', 'relevance', 'ok', 5)
938
+ condition('C8', 'witness_integrity', 'ok', 0)
914
939
  steps(3)
915
940
  verified(3)
916
941
  recomputed(0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josd/eyecode",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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
@@ -8,7 +8,7 @@ import { VERSION } from './version.js';
8
8
  export const HELP = `Usage: eyecode [OPTION ...] [FILE ...]
9
9
  Programs are JavaScript; forward rules run to a fixpoint.
10
10
  --proof Print the claims with clause(...) and step(...) records
11
- --check-proof FILE Print a C1-C7 check report of a proof (- for stdin)
11
+ --check-proof FILE Print a C1-C8 check report of a proof (- for stdin)
12
12
  --json Print the check report as JSON instead
13
13
  --goal GOAL Ask a backward goal after forward reasoning; with
14
14
  --check-proof, the goal the proof answers
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Facts and rules are JavaScript. Forward rules materialize conclusions until
4
4
  // a fixpoint, backward rules are decided when a goal asks for them, and every
5
5
  // answer can come with a proof that a separate checker verifies against the
6
- // program, condition by condition (C1-C7).
6
+ // program, condition by condition (C1-C8).
7
7
  export { VERSION } from './version.js';
8
8
  export { Result, run, unusedClauses } from './engine.js';
9
9
  export { readGoal } from './goals.js';
package/src/proof.js CHANGED
@@ -1,4 +1,4 @@
1
- // Proof documents: rendering, and checking C1-C7 against the source program.
1
+ // Proof documents: rendering, and checking C1-C8 against the source program.
2
2
  //
3
3
  // A proof document is JavaScript source, one expression per line: first the
4
4
  // claims, then clause(N, Display) for every source clause the proof cites,
@@ -15,7 +15,7 @@ import { CONTROL_KEYS, Program } from './program.js';
15
15
  import { readTerms } from './reader.js';
16
16
  import {
17
17
  Env, EyecodeError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
18
- isCallable, isGround, isTerm, key, listFromItems, properListItems, unify,
18
+ isCallable, isGround, isTerm, key, listFromItems, properListItems, unify, variables,
19
19
  } from './terms.js';
20
20
  import { Lettering, letterName, quote, write, writeNotingVariables } from './writer.js';
21
21
 
@@ -209,6 +209,7 @@ function check(program, claims, steps, goals, allowTrusted, fail, spellings, nam
209
209
  { id: 'C5', name: 're_decision', covered: tally.redecided + tally.composed, failed: failed('C5') },
210
210
  { id: 'C6', name: 'boundary_consistency', covered: confronted, failed: failed('C6') },
211
211
  { id: 'C7', name: 'relevance', covered: claims.length + steps.size, failed: failed('C7') },
212
+ { id: 'C8', name: 'witness_integrity', covered: tally.witnessed, failed: failed('C8') },
212
213
  ];
213
214
  return {
214
215
  valid: !fail.items.length,
@@ -294,7 +295,7 @@ const CONTROL_BY = 'control';
294
295
  // instance of the clause it cites, a recomputed primitive, a control composed
295
296
  // of its uses, or a trusted boundary the later checks confront.
296
297
  function checkSteps(program, steps, allowTrusted, fail, spellings, name) {
297
- const tally = { verified: 0, redecided: 0, composed: 0, boundaries: [], trusted: [] };
298
+ const tally = { verified: 0, redecided: 0, composed: 0, witnessed: 0, boundaries: [], trusted: [] };
298
299
 
299
300
  function covered(goal) {
300
301
  if (isTerm(goal, 'and', 2)) return flattenConjunction(goal).every(covered);
@@ -316,7 +317,7 @@ function checkSteps(program, steps, allowTrusted, fail, spellings, name) {
316
317
  if (!covered(use)) fail.add('C4', `unjustified use ${text(use)}`, goal);
317
318
  }
318
319
  if (isTerm(by, 'clause', 1)) {
319
- if (checkResolution(program, step, fail)) tally.verified++;
320
+ if (checkResolution(program, step, fail, tally)) tally.verified++;
320
321
  } else if (by === BUILTIN_BY) {
321
322
  if (bindings.length || uses.length || !isCallable(goal) || !PRIMITIVE_KEYS.has(key(goal))) {
322
323
  fail.add('C3', 'invalid builtin justification', goal);
@@ -415,8 +416,9 @@ function flatResolution(clause, goal, bindings, uses) {
415
416
 
416
417
  // C1: a clause step names a source clause, its bindings name distinct
417
418
  // variables of that clause, and under them one of the clause's heads is the
418
- // step's goal and its body is exactly the step's uses.
419
- function checkResolution(program, step, fail) {
419
+ // step's goal and its body is exactly the step's uses. C8 is checked under
420
+ // the same substitution.
421
+ function checkResolution(program, step, fail, tally) {
420
422
  const { goal, by, bindings, uses } = step;
421
423
  const clauseId = by.args[0];
422
424
  const clause = clauseNamed(program, clauseId);
@@ -444,6 +446,7 @@ function checkResolution(program, step, fail) {
444
446
  }
445
447
  const heads = clause.forward ? flattenConjunction(head) : [head];
446
448
  let resolution = false;
449
+ let witnesses = null;
447
450
  for (const candidate of heads) {
448
451
  const mark = env.mark();
449
452
  let matches = unify(candidate, goal, env) && body.length === uses.length;
@@ -452,6 +455,9 @@ function checkResolution(program, step, fail) {
452
455
  }
453
456
  if (matches && resolvesTo(candidate, env, goal) && body.every((item, i) => resolvesTo(item, env, uses[i]))) {
454
457
  resolution = true;
458
+ // Any head the step can be an instance of decides; a verified witness wins.
459
+ const found = clause.forward ? checkWitnesses(clause, head, names, env) : null;
460
+ if (found !== null && witnesses !== true) witnesses = found;
455
461
  }
456
462
  env.undo(mark);
457
463
  }
@@ -459,9 +465,65 @@ function checkResolution(program, step, fail) {
459
465
  fail.add('C1', `not an instance of source clause ${clauseId}: ${text(goal)}`, goal);
460
466
  return false;
461
467
  }
468
+ if (witnesses === false) fail.add('C8', `not the witness of this firing of clause ${clauseId}: ${text(goal)}`, goal);
469
+ else if (witnesses === true) tally.witnessed++;
462
470
  return true;
463
471
  }
464
472
 
473
+ // The variables of a forward rule's conclusions that its premise does not
474
+ // mention: each stands for something that exists, its witness.
475
+ function witnessNames(clause) {
476
+ if (clause.witnessNames === undefined) {
477
+ const inBody = new Set();
478
+ for (const goal of clause.body) for (const name of variables(goal).keys()) inBody.add(name);
479
+ clause.witnessNames = [...variables(clause.head).keys()].filter((name) => !inBody.has(name));
480
+ }
481
+ return clause.witnessNames;
482
+ }
483
+
484
+ // C8: under a step's substitution, each witness of a forward rule is the
485
+ // Skolem term of this firing, skolem(Rule, Variable, Arguments), where
486
+ // Arguments are the ground values of the conclusions' other variables, in
487
+ // order of first occurrence, leaving out those that hold a witness of this
488
+ // firing: the term the reasoner makes (Section 7.2). Returns null when the
489
+ // rule has no witnesses, else whether they all hold; head is the rule's
490
+ // renamed conclusions and names maps its variables to their renamed ones.
491
+ function checkWitnesses(clause, head, names, env) {
492
+ const witnessed = witnessNames(clause);
493
+ if (!witnessed.length) return null;
494
+ let firing = null;
495
+ for (const name of witnessed) {
496
+ const value = copyResolved(names.get(name), env);
497
+ if (!isTerm(value, 'skolem', 3) || value.args[0] !== BigInt(clause.id) || !witnessName(value.args[1], name)) return false;
498
+ if (firing === null) firing = value.args[2];
499
+ else if (text(value.args[2]) !== text(firing)) return false;
500
+ }
501
+ const ofThisFiring = (term) => {
502
+ const pending = [term];
503
+ while (pending.length) {
504
+ const item = pending.pop();
505
+ if (!(item instanceof Struct)) continue;
506
+ if (item.name === 'skolem' && item.args.length === 3 && item.args[0] === BigInt(clause.id) &&
507
+ text(item.args[2]) === text(firing)) return true;
508
+ pending.push(...item.args);
509
+ }
510
+ return false;
511
+ };
512
+ const expected = [];
513
+ for (const variable of variables(head).values()) {
514
+ const value = copyResolved(variable, env);
515
+ if (isGround(value) && !ofThisFiring(value)) expected.push(value);
516
+ }
517
+ return text(listFromItems(expected)) === text(firing);
518
+ }
519
+
520
+ // The name a witness of variable name carries: the name itself, or with a
521
+ // number when a value of the firing brought in a variable of the same name.
522
+ function witnessName(written, name) {
523
+ if (typeof written !== 'string' || !written.startsWith(name)) return false;
524
+ return written === name || /^[0-9]+$/.test(written.slice(name.length));
525
+ }
526
+
465
527
  // C6: a trusted boundary cannot be proved, but evidence at hand can refute
466
528
  // it. An absence fails when a source fact, a step of this certificate or a
467
529
  // 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.5';
2
+ export const VERSION = '0.1.6';