@josd/eyecode 0.1.4 → 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 +1 -1
- package/SPEC.md +40 -15
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/index.js +1 -1
- package/src/lang.js +188 -6
- package/src/proof.js +68 -6
- package/src/reader.js +74 -57
- package/src/version.js +1 -1
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–
|
|
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
|
+
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
|
|
19
|
-
conditions (C1-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
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-
|
|
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-
|
|
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/lang.js
CHANGED
|
@@ -137,6 +137,10 @@ function renameAnonymous(term, counter) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Collects the clauses a program states, in order.
|
|
140
|
+
// The line of the statement call being run, when the call names it (see
|
|
141
|
+
// markStatements); otherwise statements find it on the stack.
|
|
142
|
+
let statedLine = null;
|
|
143
|
+
|
|
140
144
|
class Builder {
|
|
141
145
|
constructor(locate) {
|
|
142
146
|
this.sources = [];
|
|
@@ -144,7 +148,7 @@ class Builder {
|
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
add(kind, head, body, line = null) {
|
|
147
|
-
if (line === null) line = this.locate();
|
|
151
|
+
if (line === null) line = statedLine !== null ? statedLine : this.locate();
|
|
148
152
|
const counter = new Anonymous([head, ...body]);
|
|
149
153
|
// In the order the statement writes them: a forward rule, implies(),
|
|
150
154
|
// writes its premise first.
|
|
@@ -239,7 +243,10 @@ const LIBRARY = Object.freeze({
|
|
|
239
243
|
// The names a program is given: the statements and the terms it builds goals
|
|
240
244
|
// with. The statements report to the innermost program being built.
|
|
241
245
|
function exportsFor(io) {
|
|
242
|
-
const builder = {
|
|
246
|
+
const builder = {
|
|
247
|
+
add: (...args) => active().add(...args),
|
|
248
|
+
locate: () => (statedLine !== null ? statedLine : active().locate()),
|
|
249
|
+
};
|
|
243
250
|
const statements = {
|
|
244
251
|
fact: (...terms) => {
|
|
245
252
|
for (const term of terms) builder.add('fact', toTerm(term), []);
|
|
@@ -308,6 +315,23 @@ function exportsFor(io) {
|
|
|
308
315
|
fail: 'fail',
|
|
309
316
|
eyecode: LIBRARY,
|
|
310
317
|
};
|
|
318
|
+
const named = new Set(STATEMENT_NAMES.map((name) => statements[name]));
|
|
319
|
+
// A statement call rewritten by markStatements: the statement, run with its
|
|
320
|
+
// line known.
|
|
321
|
+
Object.defineProperty(statements, AT, {
|
|
322
|
+
value: (statement, line) => {
|
|
323
|
+
if (!named.has(statement)) return statement;
|
|
324
|
+
return (...args) => {
|
|
325
|
+
const outer = statedLine;
|
|
326
|
+
statedLine = line;
|
|
327
|
+
try {
|
|
328
|
+
return statement(...args);
|
|
329
|
+
} finally {
|
|
330
|
+
statedLine = outer;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
});
|
|
311
335
|
for (const primitiveKey of PRIMITIVE_KEYS) {
|
|
312
336
|
const name = keyName(primitiveKey);
|
|
313
337
|
const arity = Number(primitiveKey.slice(name.length + 1));
|
|
@@ -329,6 +353,9 @@ const KEPT = new Set([
|
|
|
329
353
|
function programScope(exported) {
|
|
330
354
|
const locals = new Map();
|
|
331
355
|
const predicates = new Map();
|
|
356
|
+
// Variables are matched by name and each _ is renamed where it occurs, so
|
|
357
|
+
// one Var per name serves every use.
|
|
358
|
+
const variables = new Map();
|
|
332
359
|
return new Proxy(Object.create(null), {
|
|
333
360
|
has(target, name) {
|
|
334
361
|
return typeof name === 'string' && !KEPT.has(name);
|
|
@@ -337,7 +364,15 @@ function programScope(exported) {
|
|
|
337
364
|
if (typeof name !== 'string') return undefined;
|
|
338
365
|
if (locals.has(name)) return locals.get(name);
|
|
339
366
|
if (Object.hasOwn(exported, name)) return exported[name];
|
|
340
|
-
|
|
367
|
+
const first = name.charCodeAt(0);
|
|
368
|
+
if ((first >= 65 && first <= 90) || first === 95) {
|
|
369
|
+
let variable = variables.get(name);
|
|
370
|
+
if (variable === undefined) {
|
|
371
|
+
variable = new Var(name);
|
|
372
|
+
variables.set(name, variable);
|
|
373
|
+
}
|
|
374
|
+
return variable;
|
|
375
|
+
}
|
|
341
376
|
let made = predicates.get(name);
|
|
342
377
|
if (made === undefined) {
|
|
343
378
|
made = predicate(name);
|
|
@@ -379,6 +414,146 @@ function lineOffset() {
|
|
|
379
414
|
return firstLine;
|
|
380
415
|
}
|
|
381
416
|
|
|
417
|
+
const AT = '__eyecode_at__';
|
|
418
|
+
const STATEMENT_NAMES = ['fact', 'implies', 'implied_by', 'query', 'contradiction', 'facts_from'];
|
|
419
|
+
const STATEMENT_SET = new Set(STATEMENT_NAMES);
|
|
420
|
+
const IDENTIFIER = /[A-Za-z0-9_$]/;
|
|
421
|
+
// Words after which a slash starts a regular expression rather than a division.
|
|
422
|
+
const BEFORE_EXPRESSION = new Set(['return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'throw', 'else', 'do',
|
|
423
|
+
'instanceof', 'yield', 'await']);
|
|
424
|
+
|
|
425
|
+
// The program with each call of a statement, such as fact(...), rewritten to
|
|
426
|
+
// name its own line: __eyecode_at__(fact, 12)(...). Finding the line of a call
|
|
427
|
+
// on the stack costs microseconds, which adds up over a program that states a
|
|
428
|
+
// hundred thousand clauses. The rewriting adds no line breaks, skips strings,
|
|
429
|
+
// comments, template text and regular expressions, and leaves property calls
|
|
430
|
+
// and declarations alone; a call it misses still finds its line on the stack.
|
|
431
|
+
function markStatements(source) {
|
|
432
|
+
const out = [];
|
|
433
|
+
const templates = []; // for each open brace: true when it closes a template's ${
|
|
434
|
+
let from = 0;
|
|
435
|
+
let line = 1;
|
|
436
|
+
let i = 0;
|
|
437
|
+
let previous = ''; // the previous token: an identifier, or one punctuation character
|
|
438
|
+
const n = source.length;
|
|
439
|
+
const skipTo = (end) => {
|
|
440
|
+
for (; i < end; i++) if (source[i] === '\n') line++;
|
|
441
|
+
};
|
|
442
|
+
const template = () => {
|
|
443
|
+
// From inside a template's text to its end, or to a ${ that opens code.
|
|
444
|
+
while (i < n) {
|
|
445
|
+
const ch = source[i];
|
|
446
|
+
if (ch === '\\') skipTo(i + 2);
|
|
447
|
+
else if (ch === '`') {
|
|
448
|
+
i++;
|
|
449
|
+
previous = ')';
|
|
450
|
+
return;
|
|
451
|
+
} else if (ch === '$' && source[i + 1] === '{') {
|
|
452
|
+
i += 2;
|
|
453
|
+
templates.push(true);
|
|
454
|
+
previous = '{';
|
|
455
|
+
return;
|
|
456
|
+
} else skipTo(i + 1);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
while (i < n) {
|
|
460
|
+
const ch = source[i];
|
|
461
|
+
if (ch === '\n') {
|
|
462
|
+
line++;
|
|
463
|
+
i++;
|
|
464
|
+
} else if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
465
|
+
i++;
|
|
466
|
+
} else if (ch === '/' && source[i + 1] === '/') {
|
|
467
|
+
while (i < n && source[i] !== '\n') i++;
|
|
468
|
+
} else if (ch === '/' && source[i + 1] === '*') {
|
|
469
|
+
const end = source.indexOf('*/', i + 2);
|
|
470
|
+
skipTo(end < 0 ? n : end + 2);
|
|
471
|
+
} else if (ch === "'" || ch === '"') {
|
|
472
|
+
i++;
|
|
473
|
+
while (i < n && source[i] !== ch && source[i] !== '\n') skipTo(source[i] === '\\' ? i + 2 : i + 1);
|
|
474
|
+
i++;
|
|
475
|
+
previous = ')';
|
|
476
|
+
} else if (ch === '`') {
|
|
477
|
+
i++;
|
|
478
|
+
template();
|
|
479
|
+
} else if (ch === '/' && (previous === '' || BEFORE_EXPRESSION.has(previous) || '(,=:[!&|?{};+-*%<>~^'.includes(previous))) {
|
|
480
|
+
// A regular expression literal.
|
|
481
|
+
i++;
|
|
482
|
+
let inClass = false;
|
|
483
|
+
while (i < n && source[i] !== '\n' && (inClass || source[i] !== '/')) {
|
|
484
|
+
if (source[i] === '\\') i++;
|
|
485
|
+
else if (source[i] === '[') inClass = true;
|
|
486
|
+
else if (source[i] === ']') inClass = false;
|
|
487
|
+
i++;
|
|
488
|
+
}
|
|
489
|
+
i++;
|
|
490
|
+
while (i < n && IDENTIFIER.test(source[i])) i++;
|
|
491
|
+
previous = ')';
|
|
492
|
+
} else if (IDENTIFIER.test(ch)) {
|
|
493
|
+
const start = i;
|
|
494
|
+
while (i < n && IDENTIFIER.test(source[i])) i++;
|
|
495
|
+
const word = source.slice(start, i);
|
|
496
|
+
if (STATEMENT_SET.has(word) && previous !== '.' && previous !== 'function') {
|
|
497
|
+
let next = i;
|
|
498
|
+
while (source[next] === ' ' || source[next] === '\t') next++;
|
|
499
|
+
if (source[next] === '(') {
|
|
500
|
+
out.push(source.slice(from, start), `${AT}(${word}, ${line})`);
|
|
501
|
+
from = i;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
previous = /^[0-9]/.test(word) ? ')' : word;
|
|
505
|
+
} else if (ch === '{') {
|
|
506
|
+
templates.push(false);
|
|
507
|
+
previous = '{';
|
|
508
|
+
i++;
|
|
509
|
+
} else if (ch === '}') {
|
|
510
|
+
i++;
|
|
511
|
+
if (templates.pop()) template();
|
|
512
|
+
else previous = '}';
|
|
513
|
+
} else {
|
|
514
|
+
previous = ch === ')' || ch === ']' ? ')' : ch;
|
|
515
|
+
i++;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
out.push(source.slice(from));
|
|
519
|
+
return out.join('');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// The line of the innermost frame of the script at url, or null when none is
|
|
523
|
+
// on the stack. Where the host gives stack frames as objects (V8), they are
|
|
524
|
+
// read directly, which is many times faster than formatting the stack as
|
|
525
|
+
// text for every statement; elsewhere this returns undefined and the text is
|
|
526
|
+
// searched.
|
|
527
|
+
function innermostLine(url) {
|
|
528
|
+
if (typeof Error.captureStackTrace !== 'function') return undefined;
|
|
529
|
+
// A statement is usually called from the program itself, a few frames up,
|
|
530
|
+
// so a short stack is captured first and the whole one only when needed.
|
|
531
|
+
for (const limit of [10, Infinity]) {
|
|
532
|
+
const frames = callSites(limit);
|
|
533
|
+
if (!Array.isArray(frames)) return undefined;
|
|
534
|
+
for (const frame of frames) {
|
|
535
|
+
if (frame.getScriptNameOrSourceURL() === url) return frame.getLineNumber();
|
|
536
|
+
}
|
|
537
|
+
if (frames.length < limit) break;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function callSites(limit) {
|
|
543
|
+
const prepare = Error.prepareStackTrace;
|
|
544
|
+
const stackLimit = Error.stackTraceLimit;
|
|
545
|
+
const holder = {};
|
|
546
|
+
try {
|
|
547
|
+
Error.prepareStackTrace = (_, sites) => sites;
|
|
548
|
+
Error.stackTraceLimit = limit;
|
|
549
|
+
Error.captureStackTrace(holder, innermostLine);
|
|
550
|
+
return holder.stack;
|
|
551
|
+
} finally {
|
|
552
|
+
Error.prepareStackTrace = prepare;
|
|
553
|
+
Error.stackTraceLimit = stackLimit;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
382
557
|
function compile(source, url) {
|
|
383
558
|
// eslint-disable-next-line no-new-func
|
|
384
559
|
return new Function('__eyecode_scope__', `with (__eyecode_scope__) {\n${source}\n}\n//# sourceURL=${url}`);
|
|
@@ -429,11 +604,18 @@ function execute(source, filename, builder, io) {
|
|
|
429
604
|
const url = `eyecode-program-${id}.js`;
|
|
430
605
|
let fn;
|
|
431
606
|
try {
|
|
432
|
-
fn = compile(source, url);
|
|
433
|
-
} catch
|
|
434
|
-
|
|
607
|
+
fn = compile(markStatements(source), url);
|
|
608
|
+
} catch {
|
|
609
|
+
// Should the rewriting ever break the program, it runs as written.
|
|
610
|
+
try {
|
|
611
|
+
fn = compile(source, url);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
throw new EyecodeError(`${filename}: ${error.name}: ${error.message}`);
|
|
614
|
+
}
|
|
435
615
|
}
|
|
436
616
|
builder.locate = () => {
|
|
617
|
+
const line = innermostLine(url);
|
|
618
|
+
if (line !== undefined) return line === null ? null : line - lineOffset() + 1;
|
|
437
619
|
const lines = frameLines(new Error().stack, id);
|
|
438
620
|
return lines.length ? lines[0] - lineOffset() + 1 : null;
|
|
439
621
|
};
|
package/src/proof.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Proof documents: rendering, and checking C1-
|
|
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
|
-
|
|
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/reader.js
CHANGED
|
@@ -22,77 +22,85 @@ function variableName(text, line) {
|
|
|
22
22
|
|
|
23
23
|
// Numbers and names of any length, matched where the scan stands.
|
|
24
24
|
const NUMBER = /(?:([0-9]+)n|((?:[0-9]+\.[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+)|([0-9]+))/y;
|
|
25
|
-
const NAME = /[A-Za-z0-9_$]+/y;
|
|
26
25
|
|
|
27
26
|
const SIMPLE_ESCAPES = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0', "'": "'", '"': '"', '\\': '\\' };
|
|
28
27
|
|
|
28
|
+
const isDigit = (code) => code >= 48 && code <= 57;
|
|
29
|
+
const isNameStart = (code) => (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95 || code === 36;
|
|
30
|
+
const isNamePart = (code) => isNameStart(code) || isDigit(code);
|
|
31
|
+
|
|
32
|
+
// The tokens of a text, read one ahead of the parser as it asks for them, so
|
|
33
|
+
// a large document is never held as tokens all at once.
|
|
29
34
|
class Tokens {
|
|
30
35
|
constructor(text) {
|
|
31
36
|
this.text = text;
|
|
32
37
|
this.pos = 0;
|
|
33
38
|
this.line = 1;
|
|
34
|
-
this.
|
|
35
|
-
this.scan();
|
|
36
|
-
this.index = 0;
|
|
39
|
+
this.current = this.read();
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
error(message, line = this.line) {
|
|
40
43
|
return new EyecodeError(`line ${line}: ${message}`);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
// The token at this.pos, moving past it.
|
|
47
|
+
read() {
|
|
44
48
|
const { text } = this;
|
|
45
49
|
const n = text.length;
|
|
46
|
-
let pos =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
50
|
+
let { pos, line } = this;
|
|
51
|
+
try {
|
|
52
|
+
while (pos < n) {
|
|
53
|
+
const ch = text[pos];
|
|
54
|
+
const code = ch.charCodeAt(0);
|
|
55
|
+
if (ch === '\n') {
|
|
56
|
+
pos++;
|
|
57
|
+
line++;
|
|
58
|
+
return { kind: 'newline', text: '\n', line: line - 1 };
|
|
59
|
+
} else if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\ufeff') {
|
|
60
|
+
pos++;
|
|
61
|
+
} else if (ch === '/' && text[pos + 1] === '/') {
|
|
62
|
+
while (pos < n && text[pos] !== '\n') pos++;
|
|
63
|
+
} else if (ch === '/' && text[pos + 1] === '*') {
|
|
64
|
+
const end = text.indexOf('*/', pos + 2);
|
|
65
|
+
if (end < 0) throw this.error('unterminated comment', line);
|
|
66
|
+
for (let i = pos; i < end; i++) if (text[i] === '\n') line++;
|
|
67
|
+
pos = end + 2;
|
|
68
|
+
} else if (ch === "'" || ch === '"') {
|
|
69
|
+
const [value, next] = this.string(pos, line);
|
|
70
|
+
pos = next;
|
|
71
|
+
return { kind: 'string', text: value, line };
|
|
72
|
+
} else if (isDigit(code) || (ch === '.' && isDigit(text.charCodeAt(pos + 1)))) {
|
|
73
|
+
NUMBER.lastIndex = pos;
|
|
74
|
+
const match = NUMBER.exec(text);
|
|
75
|
+
const after = text.charCodeAt(pos + match[0].length);
|
|
76
|
+
if (isNamePart(after) || after === 46) throw this.error(`malformed number ${text.slice(pos, pos + match[0].length + 1)}`, line);
|
|
77
|
+
pos += match[0].length;
|
|
78
|
+
if (match[1] !== undefined || match[3] !== undefined) {
|
|
79
|
+
const digits = match[1] !== undefined ? match[1] : match[3];
|
|
80
|
+
if (digits.length > 1 && digits[0] === '0') throw this.error(`an integer has no leading zero: ${digits}`, line);
|
|
81
|
+
return { kind: 'int', text: digits, line };
|
|
82
|
+
}
|
|
83
|
+
return { kind: 'float', text: match[2], line };
|
|
84
|
+
} else if (isNameStart(code)) {
|
|
85
|
+
const start = pos;
|
|
86
|
+
pos++;
|
|
87
|
+
while (pos < n && isNamePart(text.charCodeAt(pos))) pos++;
|
|
88
|
+
return { kind: 'name', text: text.slice(start, pos), line };
|
|
89
|
+
} else if (ch === '.' && text[pos + 1] === '.' && text[pos + 2] === '.') {
|
|
90
|
+
pos += 3;
|
|
91
|
+
return { kind: 'punct', text: '...', line };
|
|
92
|
+
} else if ('()[]{},:;-'.includes(ch)) {
|
|
93
|
+
pos++;
|
|
94
|
+
return { kind: 'punct', text: ch, line };
|
|
76
95
|
} else {
|
|
77
|
-
this.
|
|
96
|
+
throw this.error(`unexpected character ${JSON.stringify(ch)}`, line);
|
|
78
97
|
}
|
|
79
|
-
pos += match[0].length;
|
|
80
|
-
} else if (/[A-Za-z_$]/.test(ch)) {
|
|
81
|
-
NAME.lastIndex = pos;
|
|
82
|
-
const match = NAME.exec(text);
|
|
83
|
-
this.list.push({ kind: 'name', text: match[0], line });
|
|
84
|
-
pos += match[0].length;
|
|
85
|
-
} else if (text.startsWith('...', pos)) {
|
|
86
|
-
this.list.push({ kind: 'punct', text: '...', line });
|
|
87
|
-
pos += 3;
|
|
88
|
-
} else if ('()[]{},:;-'.includes(ch)) {
|
|
89
|
-
this.list.push({ kind: 'punct', text: ch, line });
|
|
90
|
-
pos++;
|
|
91
|
-
} else {
|
|
92
|
-
throw this.error(`unexpected character ${JSON.stringify(ch)}`, line);
|
|
93
98
|
}
|
|
99
|
+
return { kind: 'end', text: '', line };
|
|
100
|
+
} finally {
|
|
101
|
+
this.pos = pos;
|
|
102
|
+
this.line = line;
|
|
94
103
|
}
|
|
95
|
-
this.list.push({ kind: 'end', text: '', line });
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
string(start, line) {
|
|
@@ -109,8 +117,15 @@ class Tokens {
|
|
|
109
117
|
// A document spells control characters as escapes, as the writer does.
|
|
110
118
|
if (ch < ' ' && ch !== '\t') throw this.error('a control character in a string must be escaped', line);
|
|
111
119
|
if (ch !== '\\') {
|
|
112
|
-
|
|
113
|
-
pos
|
|
120
|
+
// A run of plain characters at once.
|
|
121
|
+
let end = pos + 1;
|
|
122
|
+
while (end < text.length) {
|
|
123
|
+
const code = text.charCodeAt(end);
|
|
124
|
+
if (code < 32 || code === 92 || text[end] === quote) break;
|
|
125
|
+
end++;
|
|
126
|
+
}
|
|
127
|
+
out += text.slice(pos, end);
|
|
128
|
+
pos = end;
|
|
114
129
|
continue;
|
|
115
130
|
}
|
|
116
131
|
const next = text[pos + 1];
|
|
@@ -140,20 +155,22 @@ class Tokens {
|
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
peek() {
|
|
143
|
-
return this.
|
|
158
|
+
return this.current;
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
next() {
|
|
147
|
-
|
|
162
|
+
const token = this.current;
|
|
163
|
+
if (token.kind !== 'end') this.current = this.read();
|
|
164
|
+
return token;
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
// Newlines inside brackets are layout.
|
|
151
168
|
skipNewlines() {
|
|
152
|
-
while (this.
|
|
169
|
+
while (this.current.kind === 'newline') this.current = this.read();
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
at(text) {
|
|
156
|
-
const token = this.
|
|
173
|
+
const token = this.current;
|
|
157
174
|
return token.kind === 'punct' && token.text === text;
|
|
158
175
|
}
|
|
159
176
|
|
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.
|
|
2
|
+
export const VERSION = '0.1.6';
|