eyemind 0.1.25 → 0.1.27
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 +3 -2
- package/SPEC.md +200 -24
- package/package.json +1 -1
- package/src/cli.js +23 -8
- package/src/engine.js +108 -18
- package/src/goals.js +7 -4
- package/src/inspection.js +17 -0
- package/src/lang.js +48 -8
- package/src/program.js +38 -6
- package/src/proof.js +46 -4
- package/src/templates.js +99 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ JavaScript library, and a browser application, with no dependencies.
|
|
|
16
16
|
- **[Specification](https://eyereasoner.github.io/eyemind/SPEC)**: the
|
|
17
17
|
language, its canonical text, the proof format, and the C1–C8 checks.
|
|
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/)**:
|
|
21
|
+
- **[Conformance suite](conformance/)**: 384 cases that test any
|
|
22
22
|
implementation of the specification through its command line.
|
|
23
23
|
|
|
24
24
|
```js
|
|
@@ -30,6 +30,7 @@ query(type(X, Y))
|
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
node bin/eyemind.js examples/socrates.js
|
|
33
|
+
node bin/eyemind.js -c examples/socrates.js
|
|
33
34
|
node bin/eyemind.js --proof examples/socrates.js > /tmp/proof.js
|
|
34
35
|
node bin/eyemind.js --check-proof /tmp/proof.js examples/socrates.js
|
|
35
36
|
```
|
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.
|
|
5
|
+
Version: eyemind 0.1.27
|
|
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.
|
|
25
|
+
the language and formats as implemented by eyemind 0.1.27, 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.
|
|
@@ -145,7 +145,7 @@ loaded as one program; their clauses are then numbered in the order the
|
|
|
145
145
|
scripts are run.
|
|
146
146
|
|
|
147
147
|
A program is given exactly the names needed to state it: the statements of
|
|
148
|
-
Section 4.2, `preds`, `vars`, `struct`, the controls and primitives of
|
|
148
|
+
Section 4.2, `forward`, `backward`, `preds`, `vars`, `struct`, the controls and primitives of
|
|
149
149
|
Section 5, the atom `fail`, and `eyemind`, the library, with which a program
|
|
150
150
|
can build and run another program and read and write documents
|
|
151
151
|
(`eyemind.build`, `eyemind.run`, `eyemind.checkProof`, `eyemind.checkReport`,
|
|
@@ -166,7 +166,14 @@ A premise is a goal or an array of goals, their conjunction (Section 5.1). A
|
|
|
166
166
|
body goal that is a conjunction is split into its conjuncts; the clause body
|
|
167
167
|
is the resulting sequence of goals.
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
`implies` and `implied_by` take exactly two arguments. Each call also returns
|
|
170
|
+
a rule value; when that value is embedded in another statement of the same
|
|
171
|
+
program build, the call contributes a template instead of a standalone clause
|
|
172
|
+
(Section 4.6). An ignored or unembedded return value keeps its standalone
|
|
173
|
+
clause.
|
|
174
|
+
|
|
175
|
+
Clauses are numbered from 1 in the order they are stated, omitting embedded
|
|
176
|
+
rule calls. Each clause records
|
|
170
177
|
the line of the program on which the statement stating it begins; the facts
|
|
171
178
|
of `facts_from` record the line of that call.
|
|
172
179
|
|
|
@@ -180,7 +187,8 @@ statements) would run as JavaScript code.
|
|
|
180
187
|
A clause MUST be rejected when:
|
|
181
188
|
|
|
182
189
|
- a head is not an atom or a compound;
|
|
183
|
-
- a head is `step/4` or `clause/2`, which are reserved for proof documents
|
|
190
|
+
- a head is `step/4` or `clause/2`, which are reserved for proof documents,
|
|
191
|
+
or explicitly states `activated/2`, reserved for template activations;
|
|
184
192
|
- a head is a primitive (Section 5.2) or a control (Section 5.1), except the
|
|
185
193
|
head `'true'` of a query and `'false'` of a contradiction; in particular a
|
|
186
194
|
conclusion of `implies` is never `'true'` or `'false'`;
|
|
@@ -230,7 +238,8 @@ Section 5.1 treats as anonymous.
|
|
|
230
238
|
|
|
231
239
|
In a goal position, an array is the conjunction of its items: a statement's
|
|
232
240
|
goal, the argument of `and`, `or`, `not`, `call` and `once`, and the goal of
|
|
233
|
-
`findall
|
|
241
|
+
`findall`, and either part of a rule template (Section 4.6). Anywhere else an
|
|
242
|
+
array is a list.
|
|
234
243
|
|
|
235
244
|
Operators do not build terms. Variables, compounds and predicates have no
|
|
236
245
|
JavaScript primitive value, so an operator applied to one, such as `X + 1` or
|
|
@@ -258,8 +267,106 @@ A program MUST be rejected when:
|
|
|
258
267
|
Because dependencies compare whole terms, two relations sharing a predicate
|
|
259
268
|
name MAY have different ranks.
|
|
260
269
|
|
|
270
|
+
### 4.6 Inline rule templates
|
|
271
|
+
|
|
272
|
+
Nested `implies(Body, Head)` and `implied_by(Head, Body)` calls construct rule
|
|
273
|
+
templates. A rule value in a conclusion MUST occur as a top-level conjunct of
|
|
274
|
+
an `implies` conclusion; in a body, `implies` and `implied_by` are inspection
|
|
275
|
+
goals (Section 4.7). The
|
|
276
|
+
conclusion MAY combine ordinary heads and several templates. Each template's
|
|
277
|
+
body and head MAY themselves be conjunctions, written as arrays. A backward
|
|
278
|
+
template with several heads defines one backward clause per head, sharing
|
|
279
|
+
the same activation and body. Empty and open lists are not goals or heads.
|
|
280
|
+
|
|
281
|
+
JavaScript evaluates inner calls first. When a statement embeds the rule
|
|
282
|
+
value returned by an earlier call in the same program build, the earlier
|
|
283
|
+
call's standalone registration MUST be removed before clause numbering and
|
|
284
|
+
validation. This applies through arrays, helpers and stored values. A value
|
|
285
|
+
MAY be reused in several generators; none of those uses restores its
|
|
286
|
+
standalone registration. Separate program builds collect their registrations
|
|
287
|
+
independently, even when a rule value is passed between them. Anonymous
|
|
288
|
+
variables are named within each enclosing clause, so anonymous variables of
|
|
289
|
+
the template cannot capture unrelated anonymous variables of the generator.
|
|
290
|
+
|
|
291
|
+
The pure constructors `forward(Body, Head)` and `backward(Head, Body)` also
|
|
292
|
+
construct templates with the same lowering and scope. They do not themselves
|
|
293
|
+
register clauses.
|
|
294
|
+
|
|
295
|
+
A template variable is **captured** when it also occurs in the generating
|
|
296
|
+
clause's body or in an ordinary sibling head. The other variables are local
|
|
297
|
+
to that template: they are universally quantified within the generated rule,
|
|
298
|
+
except for head-only variables of a forward template, which receive witnesses
|
|
299
|
+
as Section 7.2 describes. Variables shared only between sibling templates are
|
|
300
|
+
local to each. Every call in a template body, including calls inside controls,
|
|
301
|
+
MUST be statically named; a variable in a term's argument is permitted, but a
|
|
302
|
+
variable used as a goal is not. A forward template MAY conclude further inline
|
|
303
|
+
templates. A backward template MUST NOT conclude templates.
|
|
304
|
+
|
|
305
|
+
Templates are lowered before validation, indexing and stratification. Starting
|
|
306
|
+
with all stated clauses, in clause order, replace each template conjunct,
|
|
307
|
+
left to right, with `activated(N, Captures)`. `Captures` is a list of its
|
|
308
|
+
captured variables in order of first occurrence in the template as written;
|
|
309
|
+
`N` is the number of the first clause appended for it. Append a forward or
|
|
310
|
+
backward clause as specified by the template, with the activation as its first
|
|
311
|
+
body goal, followed by the template's body goals. A forward template keeps its
|
|
312
|
+
head conjunction; a backward template appends one clause per head in order.
|
|
313
|
+
Continue processing the appended clauses in order, lowering any nested
|
|
314
|
+
templates the same way. Original clause numbers are kept; appended clauses
|
|
315
|
+
are numbered consecutively after them and inherit the generator's source line.
|
|
316
|
+
Capture metadata belongs to each appended clause, including one whose head
|
|
317
|
+
is subsequently lowered again.
|
|
318
|
+
|
|
319
|
+
For example, `implies(cat(X), implies(dog(Y), test('yes')))` as the only
|
|
320
|
+
stated clause becomes these two clauses:
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
implies(cat(X), activated(2, []))
|
|
324
|
+
implies([activated(2, []), dog(Y)], test('yes'))
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`Y` stays local to the second clause. Its activation is required even though
|
|
328
|
+
the template does not capture `X`. A generator with several solutions but
|
|
329
|
+
identical captures enables the same rule instance. No arbitrary rule term is
|
|
330
|
+
installed during reasoning. All potential generated clauses and their
|
|
331
|
+
activation dependencies participate in Section 4.5, including clauses whose
|
|
332
|
+
generators never fire. A closed dependency cycle MUST therefore be rejected
|
|
333
|
+
even if its template would remain inactive.
|
|
334
|
+
|
|
335
|
+
### 4.7 Source rule inspection
|
|
336
|
+
|
|
337
|
+
As goals, `implies(Body, Head)` and `implied_by(Head, Body)` match ordinary
|
|
338
|
+
forward and backward source rules respectively. The fixed catalogue contains
|
|
339
|
+
only original `implies` and `implied_by` clauses whose heads contain no rule
|
|
340
|
+
templates and whose bodies contain no inspection goals, including inside
|
|
341
|
+
controls. Facts, queries, contradictions, template generators and appended
|
|
342
|
+
clauses are excluded. Inspecting rules therefore cannot inspect themselves.
|
|
343
|
+
|
|
344
|
+
The catalogue keeps source order. A rule's body and forward head conjunctions
|
|
345
|
+
are terms `and(...)`, as normalized goals are, and inspection arguments are
|
|
346
|
+
goal positions. Each attempt freshens the whole rule with one variable map:
|
|
347
|
+
shared variables stay shared, and different attempts are independent. Matching
|
|
348
|
+
can instantiate these variables. Inspection reads the rule's structure; it
|
|
349
|
+
does not solve its body or establish that its head holds.
|
|
350
|
+
|
|
351
|
+
Free variables supplied by a successful inspection are scoped universal
|
|
352
|
+
pattern variables. Forward conclusions preserve them, including through
|
|
353
|
+
backward derivations and copied collection results. Derived patterns are
|
|
354
|
+
identified up to variable renaming and freshened, together with their evidence,
|
|
355
|
+
at every use. Unrelated unbound variables keep Section 7.2's witness behavior.
|
|
356
|
+
When inspection variables remain free in a conclusion or its captured
|
|
357
|
+
template context, that firing MUST NOT introduce head-only existential
|
|
358
|
+
witnesses: it MUST fail with an error requiring the inspection variables to
|
|
359
|
+
be bound first. Section 11.9 rejects certificates that bypass this boundary.
|
|
360
|
+
|
|
361
|
+
Inspection uses a fixed source catalogue, so its own results do not depend on
|
|
362
|
+
forward rounds. Other goals using derived patterns still participate in
|
|
363
|
+
stratification. Negation and collection retain their explicit obligations.
|
|
364
|
+
|
|
261
365
|
## 5. Goals, Controls and Primitives
|
|
262
366
|
|
|
367
|
+
The program-dependent source inspection goals `implies/2` and `implied_by/2`
|
|
368
|
+
are defined in Section 4.7.
|
|
369
|
+
|
|
263
370
|
### 5.1 Controls
|
|
264
371
|
|
|
265
372
|
| Control | Term | Meaning |
|
|
@@ -404,7 +511,8 @@ concluded:
|
|
|
404
511
|
shared. A variable that a value of the solution brought in, rather than a
|
|
405
512
|
head variable, is named after its own variable, followed by `1`, `2`, ...
|
|
406
513
|
when that name is taken. A rule that keeps meeting new activations does
|
|
407
|
-
not terminate (Section 7.3). Derived facts
|
|
514
|
+
not terminate (Section 7.3). Derived facts other than scoped inspection
|
|
515
|
+
patterns (Section 4.7) are therefore ground. A
|
|
408
516
|
checker verifies the Skolem terms of a proof (C8, Section 11.9);
|
|
409
517
|
- a head `'true'` reports the instance of the rule's body, as a conjunction,
|
|
410
518
|
once per distinct instance (by identity of canonical text);
|
|
@@ -416,6 +524,18 @@ concluded:
|
|
|
416
524
|
When the run asks its own goals (Section 7.4), rules whose heads are all
|
|
417
525
|
`'true'` are not run.
|
|
418
526
|
|
|
527
|
+
For an appended forward clause of Section 4.6, prepend one element to
|
|
528
|
+
`Arguments`: the grounded capture list of its activation. This distinguishes
|
|
529
|
+
witnesses of different captured contexts even when a head does not mention
|
|
530
|
+
those values. An empty capture list still contributes one element, `[]`.
|
|
531
|
+
Original clauses, including generators, use the ordinary witness convention.
|
|
532
|
+
|
|
533
|
+
Scoped inspection variables of Section 4.7 are preserved instead of receiving
|
|
534
|
+
witnesses; derived conclusions may therefore be universal patterns. Each use
|
|
535
|
+
renames a pattern and its evidence apart. Such patterns are suppressed when
|
|
536
|
+
an alpha-equivalent pattern was already derived. Other derived facts keep
|
|
537
|
+
their ordinary ground identity convention.
|
|
538
|
+
|
|
419
539
|
### 7.3 Bounds
|
|
420
540
|
|
|
421
541
|
A run is bounded by `max_depth`, the depth of nested rule bodies (default
|
|
@@ -442,12 +562,16 @@ A run that concluded `'false'` has halt code 65; otherwise it has none.
|
|
|
442
562
|
|
|
443
563
|
The conclusions of a run are, in order:
|
|
444
564
|
|
|
445
|
-
1. when it halted: every derived fact, in the order derived, ending with
|
|
565
|
+
1. when it halted: every derived fact except template activations, in the order derived, ending with
|
|
446
566
|
`'false'`;
|
|
447
567
|
2. otherwise, when it was given goals: the reported instances of its goals;
|
|
448
568
|
3. otherwise, when the program has a forward rule with a head `'true'`: the
|
|
449
569
|
reported instances, in order of first report, which MAY be none;
|
|
450
|
-
4. otherwise: every derived fact, in the order derived.
|
|
570
|
+
4. otherwise: every derived fact except template activations, in the order derived.
|
|
571
|
+
|
|
572
|
+
An explicit goal MAY ask for an activation. Activations remain available as
|
|
573
|
+
premises and proof steps; the library's `inferred` list omits them. The
|
|
574
|
+
`derived` statistic counts them as facts added during reasoning.
|
|
451
575
|
|
|
452
576
|
The output of a run is the canonical text (Section 8) of each conclusion,
|
|
453
577
|
one per line.
|
|
@@ -575,6 +699,13 @@ array of its body goals when it has several, `Head` likewise the head or the
|
|
|
575
699
|
array of heads, and `G1, ...` are the body goals themselves. Displays are not
|
|
576
700
|
authority: a checker MUST compare each with the program it checks against.
|
|
577
701
|
|
|
702
|
+
For a program with templates, displays show the deterministically lowered
|
|
703
|
+
clauses of Section 4.6, including activation heads and premises. The program
|
|
704
|
+
supplied to the checker MUST be lowered from the source as Section 4.6
|
|
705
|
+
requires; displays cannot authorize extra generated rules. Each use of a
|
|
706
|
+
generated clause requires its activation as the first use, with a justified
|
|
707
|
+
step of its own. All existing checks apply.
|
|
708
|
+
|
|
578
709
|
### 10.3 Steps
|
|
579
710
|
|
|
580
711
|
`Goal` is the justified goal as it stands once the whole solution is found:
|
|
@@ -590,6 +721,7 @@ order. `By` is one of:
|
|
|
590
721
|
| `By` | Meaning | `Bindings` | `Uses` |
|
|
591
722
|
| --- | --- | --- | --- |
|
|
592
723
|
| `clause(N)` | `Goal` is an instance of a head of clause `N`, a fact or a rule, whose body instance is `Uses`. | the clause's variables | the body instance, empty for a fact |
|
|
724
|
+
| `source(N)` | `Goal` is an instance of the inspectable source rule `N` as `implies(Body, Head)` or `implied_by(Head, Body)`. | empty | empty |
|
|
593
725
|
| `'builtin'` | `Goal` is a primitive that holds. | empty | empty |
|
|
594
726
|
| `'control'` | `Goal` is `call`, `once` or a disjunction, solved by `Uses`. | empty | the solved goals |
|
|
595
727
|
| `'absent'` | `Goal` is a negation taken on trust. | empty | empty |
|
|
@@ -632,6 +764,14 @@ that afterwards the head is identical to `Goal` and each body goal to its
|
|
|
632
764
|
use. A step's own terms MUST NOT need further instantiation to match: a
|
|
633
765
|
source clause `same(X, X)` cannot justify `same('a', 'b')`.
|
|
634
766
|
|
|
767
|
+
For a `source(N)` step, `N` MUST identify an eligible source rule of Section
|
|
768
|
+
4.7. Bindings and uses MUST both be empty. The rule's inspection term,
|
|
769
|
+
freshened with one variable map, MUST unify with `Goal` and resolve exactly
|
|
770
|
+
to it without instantiating the step's own terms. This checks variable
|
|
771
|
+
sharing as well as the rule's direction and structure. The source rule is
|
|
772
|
+
read as data; its body is not derived by the checker. These steps count as
|
|
773
|
+
verified C1 steps and cite the usual source clause display.
|
|
774
|
+
|
|
635
775
|
### 11.3 C2 Well-foundedness
|
|
636
776
|
|
|
637
777
|
No step MAY depend on itself through the uses of steps.
|
|
@@ -679,7 +819,7 @@ A boundary that is not refuted remains an obligation (Section 12).
|
|
|
679
819
|
|
|
680
820
|
When goals were given and no claim is `'false'`, the questions are the goals.
|
|
681
821
|
Otherwise the questions are, for each forward rule, each head other than
|
|
682
|
-
`'true'
|
|
822
|
+
`'true'` or a template activation, and the rule's body as a conjunction when one of its heads is
|
|
683
823
|
`'true'` and no claim is `'false'`. Each claim MUST be an instance of a
|
|
684
824
|
question. Each step MUST be reachable from a conjunct of a claim through the
|
|
685
825
|
conjuncts of uses.
|
|
@@ -700,6 +840,15 @@ bound to `skolem(N, Name, Args)`, where:
|
|
|
700
840
|
the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
|
|
701
841
|
the values that do contain one hold a witness of this firing.
|
|
702
842
|
|
|
843
|
+
When a forward clause has any such variables, its full head under the step's
|
|
844
|
+
substitution MUST be ground. A generic inspection pattern cannot share one
|
|
845
|
+
existential witness across all its instances (Section 4.7).
|
|
846
|
+
|
|
847
|
+
For an appended forward clause of Section 4.6, prepend the capture list,
|
|
848
|
+
instantiated under the step's substitution, as one element of `Args`, as
|
|
849
|
+
required by Section 7.2. The checker MUST compute this from the source
|
|
850
|
+
template's capture metadata and the substitution, not from the witness term.
|
|
851
|
+
|
|
703
852
|
A variable of the head that also occurs in the body is not checked by C8,
|
|
704
853
|
even when the body leaves it unbound; its value comes from the uses, which
|
|
705
854
|
other conditions check. A step citing a rule without such variables is not
|
|
@@ -709,7 +858,7 @@ covered by C8.
|
|
|
709
858
|
|
|
710
859
|
| Condition | Covered |
|
|
711
860
|
| --- | --- |
|
|
712
|
-
| C1 resolution | `clause` steps that passed C1 |
|
|
861
|
+
| C1 resolution | `clause` and `source` steps that passed C1 |
|
|
713
862
|
| C2 well_founded | steps |
|
|
714
863
|
| C3 justification | steps |
|
|
715
864
|
| C4 coverage | claims plus uses |
|
|
@@ -719,7 +868,8 @@ covered by C8.
|
|
|
719
868
|
| C8 witness_integrity | `clause` steps of forward rules with witnesses that passed C8 |
|
|
720
869
|
|
|
721
870
|
A check also counts the **steps** it read, the **claims**, the `clause` steps
|
|
722
|
-
**verified** by C1, the `
|
|
871
|
+
**verified** by C1, the `source` inspection steps also **verified** by C1,
|
|
872
|
+
the `'builtin'` steps **recomputed** by C5, the
|
|
723
873
|
`'control'` steps **composed** by C5, and the `'absent'` and `'collected'`
|
|
724
874
|
steps **trusted**.
|
|
725
875
|
|
|
@@ -736,8 +886,9 @@ leave conclusions out, and a run's own proof holds exactly its conclusions.
|
|
|
736
886
|
|
|
737
887
|
## 12. Check Reports
|
|
738
888
|
|
|
739
|
-
A check report is a document (Section 9)
|
|
740
|
-
line
|
|
889
|
+
A check report is a document (Section 9) beginning with the proof's claims,
|
|
890
|
+
in proof order, in canonical text, one per line, followed by a blank line and
|
|
891
|
+
the following report facts, one per line:
|
|
741
892
|
|
|
742
893
|
1. `condition(Id, Name, Outcome, Covered)` for C1 to C8, where `Outcome` is
|
|
743
894
|
`'ok'` or `failed(N)`, `N` the number of that condition's failures;
|
|
@@ -751,6 +902,15 @@ line:
|
|
|
751
902
|
otherwise `'checked_with_obligations'` when there are obligations,
|
|
752
903
|
otherwise `'checked'`.
|
|
753
904
|
|
|
905
|
+
Comments MUST identify these initial terms as checked answers only when the
|
|
906
|
+
report is valid with no obligations. A valid report with obligations MUST
|
|
907
|
+
label them as conditional on those obligations. A failed report MUST label
|
|
908
|
+
them as unverified claims; their inclusion does not validate them. A valid
|
|
909
|
+
report with no claims MUST say it establishes no conclusions. Comments also
|
|
910
|
+
explain the C1-C8 order, failures and obligations when present, the statistics,
|
|
911
|
+
and that the verdict does not establish completeness. Comments are informative
|
|
912
|
+
and MAY be worded differently. They are ignored when the document is read.
|
|
913
|
+
|
|
754
914
|
Failures are listed in the order they are found: those of reading the
|
|
755
915
|
document (Section 11.1), in document order; C4 for each claim, in claim
|
|
756
916
|
order; then for each step, in document order, C4 for its uses and the C1, C3,
|
|
@@ -763,7 +923,7 @@ it differently. eyemind's details are, by condition:
|
|
|
763
923
|
|
|
764
924
|
| Condition | Details |
|
|
765
925
|
| --- | --- |
|
|
766
|
-
| C1 | `clause display differs from source`, `unknown clause ...`, `not an instance of source clause N: ...` |
|
|
926
|
+
| C1 | `clause display differs from source`, `unknown clause ...`, `not an instance of source clause N: ...`, `not an instance of inspectable source rule N: ...` |
|
|
767
927
|
| C2 | `cyclic derivation at ...` |
|
|
768
928
|
| C3 | the reader's error, `step bindings must be an object and uses an array`, `duplicate justification for ...`, `invalid builtin justification`, `trusted boundaries cannot have bindings or uses`, `unknown justification ...` |
|
|
769
929
|
| C4 | `unjustified claim ...`, `unjustified use ...` |
|
|
@@ -772,11 +932,13 @@ it differently. eyemind's details are, by condition:
|
|
|
772
932
|
| C7 | `claim answers no goal: ...`, `step serves no claim: ...` |
|
|
773
933
|
| C8 | `not the witness of this firing of clause N: ...` |
|
|
774
934
|
|
|
775
|
-
The
|
|
935
|
+
The initial answers keep the variable names of the proof. The variables of
|
|
936
|
+
each subsequent report fact are renamed `A`, `B`, ..., `Z`, `A1`, ... in order
|
|
776
937
|
of first occurrence. A report is ordinary data: `facts_from` (Section 4.2)
|
|
777
938
|
states it as facts.
|
|
778
939
|
|
|
779
|
-
The JSON form
|
|
940
|
+
The JSON form includes `answers`, an array of the initial claims' canonical
|
|
941
|
+
texts, including unverified claims when `valid` is false. It also has `valid` (boolean), `steps`,
|
|
780
942
|
`claims`, `verified`, `redecided` (the count `recomputed`), `composed`,
|
|
781
943
|
`uses` (the number of uses of all steps, integers), `trusted`
|
|
782
944
|
(a list of `{kind, conclusion}`), `failures` (a list of
|
|
@@ -815,18 +977,22 @@ eyemind [OPTION ...] [FILE ...]
|
|
|
815
977
|
| --- | --- |
|
|
816
978
|
| (none) | Load the files, or standard input, as one program and print its conclusions. |
|
|
817
979
|
| `-p`, `--proof` | Print the proof document instead. |
|
|
818
|
-
| `-c
|
|
819
|
-
| `--
|
|
980
|
+
| `-c`, `--prove-and-check` | Generate a proof, check the rendered proof against the input program, and print its answers and check report. |
|
|
981
|
+
| `--check-proof FILE` | Check the proof in `FILE` (`-` for standard input) against the program and print its answers and report. |
|
|
982
|
+
| `--json` | With either checking mode, print the report, including `answers`, as JSON. |
|
|
820
983
|
| `--goal GOAL` | Ask `GOAL`, read as in Section 9, where an array is a conjunction; MAY be repeated. With `--check-proof`, the goals the proof answers. |
|
|
821
|
-
| `--strict-proof` | With
|
|
984
|
+
| `--strict-proof` | With either checking mode, forbid trusted boundaries. |
|
|
822
985
|
| `--unused` | Print the unused clauses (Section 13). |
|
|
823
986
|
| `--stats` | Print reasoning statistics as JSON to standard error: `inferences`, the goals started, which `--max-inferences` bounds; `rounds`, the forward rounds run over all strata; and `derived`, the facts forward rules added, not counting a conclusion the program states as a fact, a query's answers or `'false'`. |
|
|
824
987
|
| `--max-depth N`, `--max-iterations N`, `--max-inferences N` | The bounds of Section 7.3; `N` MUST be a positive safe integer, at most 9007199254740991. |
|
|
825
988
|
| `-v`, `--version` | Print the version. |
|
|
826
989
|
| `-h`, `--help` | Print the usage, including every option and its short alias where one exists. |
|
|
827
990
|
|
|
828
|
-
`--
|
|
829
|
-
|
|
991
|
+
`--proof`, `--prove-and-check`, `--check-proof` and `--unused` are mutually
|
|
992
|
+
exclusive. `--json` and `--strict-proof` need a checking mode. With
|
|
993
|
+
`--prove-and-check`, reasoning bounds and `--stats` apply to proof generation;
|
|
994
|
+
an invalid check returns 2, otherwise the run's halt code is retained
|
|
995
|
+
(65 for a contradiction, 0 otherwise). A program
|
|
830
996
|
text that has a line starting with `step(` is a proof document; given one as
|
|
831
997
|
a program, the command line MUST say how to check it instead.
|
|
832
998
|
|
|
@@ -868,8 +1034,8 @@ conclusions and proof documents in the canonical text, and checks every proof
|
|
|
868
1034
|
it produces as Section 11 describes. A conforming **checker** implements
|
|
869
1035
|
Sections 9, 11 and 12 and does not depend on a reasoner. For the same
|
|
870
1036
|
program, a conforming reasoner and checker MUST produce the same
|
|
871
|
-
conclusions, proof documents and reports as eyemind 0.1.
|
|
872
|
-
except for the `Detail` texts of failures (Section 12), the internal names of
|
|
1037
|
+
conclusions, proof documents and reports as eyemind 0.1.27, byte for byte,
|
|
1038
|
+
except for comments and the `Detail` texts of failures (Section 12), the internal names of
|
|
873
1039
|
variables where Section 3 says so, and where the host's floating-point
|
|
874
1040
|
library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
|
|
875
1041
|
`pow` of floats) differ in the last digit.
|
|
@@ -945,6 +1111,12 @@ step(subclass_of('human', 'mortal'), clause(2), {}, [])
|
|
|
945
1111
|
Its check report (Section 12):
|
|
946
1112
|
|
|
947
1113
|
```js
|
|
1114
|
+
// Checked answers: these follow from the input program.
|
|
1115
|
+
type('socrates', 'mortal')
|
|
1116
|
+
type('socrates', 'human')
|
|
1117
|
+
|
|
1118
|
+
// C1-C8: resolution, acyclicity, justification, coverage,
|
|
1119
|
+
// re-decision, boundary consistency, relevance and witness integrity.
|
|
948
1120
|
condition('C1', 'resolution', 'ok', 3)
|
|
949
1121
|
condition('C2', 'well_founded', 'ok', 3)
|
|
950
1122
|
condition('C3', 'justification', 'ok', 3)
|
|
@@ -953,11 +1125,15 @@ condition('C5', 're_decision', 'ok', 0)
|
|
|
953
1125
|
condition('C6', 'boundary_consistency', 'ok', 0)
|
|
954
1126
|
condition('C7', 'relevance', 'ok', 5)
|
|
955
1127
|
condition('C8', 'witness_integrity', 'ok', 0)
|
|
1128
|
+
|
|
1129
|
+
// Proof statistics: recorded steps and how they were checked.
|
|
956
1130
|
steps(3)
|
|
957
1131
|
verified(3)
|
|
958
1132
|
recomputed(0)
|
|
959
1133
|
composed(0)
|
|
960
1134
|
trusted(0)
|
|
961
1135
|
claims(2)
|
|
1136
|
+
|
|
1137
|
+
// This verdict applies to the claims above; it does not establish completeness.
|
|
962
1138
|
verdict('checked')
|
|
963
1139
|
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { VERSION } from './version.js';
|
|
|
8
8
|
export const HELP = `Usage: eyemind [OPTION ...] [FILE ...]
|
|
9
9
|
Programs are JavaScript; forward rules run to a fixpoint.
|
|
10
10
|
-p, --proof Print the claims with clause(...) and step(...) records
|
|
11
|
-
-c, --check
|
|
11
|
+
-c, --prove-and-check Generate a proof and check it against the input program
|
|
12
|
+
--check-proof FILE Print checked answers and a C1-C8 report (- for stdin)
|
|
12
13
|
--json Print the check report as JSON instead
|
|
13
14
|
--goal GOAL Ask a backward goal after forward reasoning; with
|
|
14
15
|
--check-proof, the goal the proof answers
|
|
@@ -46,6 +47,7 @@ export function main(argv, io) {
|
|
|
46
47
|
let strictProof = false;
|
|
47
48
|
let asJson = false;
|
|
48
49
|
let unused = false;
|
|
50
|
+
let proveAndCheck = false;
|
|
49
51
|
for (let i = 0; i < argv.length; i++) {
|
|
50
52
|
const arg = argv[i];
|
|
51
53
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -58,6 +60,8 @@ export function main(argv, io) {
|
|
|
58
60
|
}
|
|
59
61
|
if (arg === '--proof' || arg === '-p') {
|
|
60
62
|
options.proof = true;
|
|
63
|
+
} else if (arg === '--prove-and-check' || arg === '-c') {
|
|
64
|
+
proveAndCheck = true;
|
|
61
65
|
} else if (arg === '--stats') {
|
|
62
66
|
stats = true;
|
|
63
67
|
} else if (arg === '--strict-proof') {
|
|
@@ -66,11 +70,11 @@ export function main(argv, io) {
|
|
|
66
70
|
asJson = true;
|
|
67
71
|
} else if (arg === '--unused') {
|
|
68
72
|
unused = true;
|
|
69
|
-
} else if (arg === '--check-proof' || arg === '
|
|
73
|
+
} else if (arg === '--check-proof' || arg === '--goal' || Object.hasOwn(LIMITS, arg)) {
|
|
70
74
|
i++;
|
|
71
75
|
if (i >= argv.length) throw new UsageError(`${arg} needs a value`);
|
|
72
76
|
const value = argv[i];
|
|
73
|
-
if (arg === '--check-proof'
|
|
77
|
+
if (arg === '--check-proof') {
|
|
74
78
|
proofFile = value;
|
|
75
79
|
} else if (arg === '--goal') {
|
|
76
80
|
goals.push(value);
|
|
@@ -85,11 +89,14 @@ export function main(argv, io) {
|
|
|
85
89
|
files.push(arg);
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
|
-
if (asJson && proofFile === null) throw new UsageError('--json requires --check-proof');
|
|
89
|
-
if (strictProof && proofFile === null) throw new UsageError('--strict-proof requires --check-proof');
|
|
92
|
+
if (asJson && proofFile === null && !proveAndCheck) throw new UsageError('--json requires --check-proof or --prove-and-check');
|
|
93
|
+
if (strictProof && proofFile === null && !proveAndCheck) throw new UsageError('--strict-proof requires --check-proof or --prove-and-check');
|
|
94
|
+
if (proveAndCheck && (proofFile !== null || options.proof)) {
|
|
95
|
+
throw new UsageError('--prove-and-check cannot be combined with --proof or --check-proof');
|
|
96
|
+
}
|
|
90
97
|
if (proofFile !== null && options.proof) throw new UsageError('--check-proof cannot be combined with --proof');
|
|
91
|
-
if (unused && (proofFile !== null || options.proof)) {
|
|
92
|
-
throw new UsageError('--unused cannot be combined with --proof or --check
|
|
98
|
+
if (unused && (proofFile !== null || options.proof || proveAndCheck)) {
|
|
99
|
+
throw new UsageError('--unused cannot be combined with --proof, --check-proof or --prove-and-check');
|
|
93
100
|
}
|
|
94
101
|
if (proofFile === '-' && (!files.length || files.includes('-'))) {
|
|
95
102
|
throw new UsageError('stdin holds the proof; name the program as files');
|
|
@@ -108,7 +115,15 @@ export function main(argv, io) {
|
|
|
108
115
|
stdout(unusedClauses(program, { goals, ...options }));
|
|
109
116
|
return 0;
|
|
110
117
|
}
|
|
111
|
-
const result = run(program, { goals, ...options });
|
|
118
|
+
const result = run(program, { goals, ...options, proof: options.proof || proveAndCheck });
|
|
119
|
+
if (proveAndCheck) {
|
|
120
|
+
// Check the rendered certificate against the input, just as a saved
|
|
121
|
+
// proof is checked; no second reasoning run supplies missing evidence.
|
|
122
|
+
const report = checkProof(program, result.proof, { goals, allowTrusted: !strictProof });
|
|
123
|
+
stdout(asJson ? `${JSON.stringify(publicReport(report), null, 2)}\n` : checkReport(report));
|
|
124
|
+
if (stats) stderr(`${JSON.stringify(result.stats)}\n`);
|
|
125
|
+
return report.valid ? result.haltCode || 0 : 2;
|
|
126
|
+
}
|
|
112
127
|
stdout(result.stdout);
|
|
113
128
|
if (stats) stderr(`${JSON.stringify(result.stats)}\n`);
|
|
114
129
|
return result.haltCode || 0;
|
package/src/engine.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { PRIMITIVE_KEYS, primitive } from './builtins.js';
|
|
3
3
|
import { freshClause, matchHead, renaming, text, variant } from './common.js';
|
|
4
4
|
import { readGoal } from './goals.js';
|
|
5
|
+
import { INSPECTION_KEYS } from './inspection.js';
|
|
5
6
|
import { checkRendered, clauseDisplay, renderProof } from './proof.js';
|
|
6
7
|
import { CONTROL_KEYS, Program } from './program.js';
|
|
8
|
+
import { isActivation } from './templates.js';
|
|
7
9
|
import {
|
|
8
10
|
Env, EyemindError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
|
|
9
11
|
identity, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems, unify,
|
|
@@ -18,7 +20,7 @@ export const COLLECTED = 'collected';
|
|
|
18
20
|
|
|
19
21
|
// One proof step: a goal, how it was justified, and the steps it used.
|
|
20
22
|
export class Node {
|
|
21
|
-
constructor(goal, by, bindings = NO_NODES, children = NO_NODES, collectedUses = null, final = false) {
|
|
23
|
+
constructor(goal, by, bindings = NO_NODES, children = NO_NODES, collectedUses = null, final = false, scoped = NO_NODES) {
|
|
22
24
|
this.goal = goal;
|
|
23
25
|
// A derived fact's node is resolved once and for all when the fact is
|
|
24
26
|
// derived, so whatever uses the fact shares the node, derivation and all.
|
|
@@ -29,6 +31,8 @@ export class Node {
|
|
|
29
31
|
// A collection has no children in a proof, so its node keeps the clauses
|
|
30
32
|
// its answers used.
|
|
31
33
|
this.collectedUses = collectedUses;
|
|
34
|
+
this.scoped = scoped;
|
|
35
|
+
this.generic = false;
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -91,7 +95,7 @@ class Point {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
count() {
|
|
94
|
-
if (this.kind === 'branch') return this.alternatives.length;
|
|
98
|
+
if (this.kind === 'branch' || this.kind === 'inspection') return this.alternatives.length;
|
|
95
99
|
return this.stop - this.start + this.clauses.length;
|
|
96
100
|
}
|
|
97
101
|
}
|
|
@@ -103,7 +107,50 @@ const SPLICE = 'splice';
|
|
|
103
107
|
function resolveOne(node, env) {
|
|
104
108
|
return new Node(copyResolved(node.goal, env), node.by,
|
|
105
109
|
node.bindings.map(([name, value]) => [name, copyResolved(value, env)]),
|
|
106
|
-
new Array(node.children.length).fill(null), node.collectedUses
|
|
110
|
+
new Array(node.children.length).fill(null), node.collectedUses, false,
|
|
111
|
+
node.scoped.map((term) => copyResolved(term, env)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// An inspected source rule supplies universal variables. Follow the recorded
|
|
115
|
+
// evidence so ordinary fresh variables still receive existential witnesses.
|
|
116
|
+
function scopedVariables(nodes, env) {
|
|
117
|
+
const found = new Map();
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
const pending = [...nodes];
|
|
120
|
+
while (pending.length) {
|
|
121
|
+
const node = pending.pop();
|
|
122
|
+
if (seen.has(node)) continue;
|
|
123
|
+
seen.add(node);
|
|
124
|
+
if (isTerm(node.by, 'source', 1)) variables(copyResolved(node.goal, env), found);
|
|
125
|
+
for (const term of node.scoped) variables(copyResolved(term, env), found);
|
|
126
|
+
pending.push(...node.children);
|
|
127
|
+
}
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A universal derived pattern is renamed together with its evidence at each
|
|
132
|
+
// use. Renaming just its head would disconnect the certificate's variables.
|
|
133
|
+
function freshNode(node, suffix) {
|
|
134
|
+
const names = new Map();
|
|
135
|
+
const copies = new Map();
|
|
136
|
+
const make = (source) => new Node(freshTerm(source.goal, suffix, names, true), source.by,
|
|
137
|
+
source.bindings.map(([name, value]) => [name, freshTerm(value, suffix, names, true)]),
|
|
138
|
+
[], source.collectedUses, false, source.scoped.map((term) => freshTerm(term, suffix, names, true)));
|
|
139
|
+
const root = make(node);
|
|
140
|
+
copies.set(node, root);
|
|
141
|
+
const pending = [node];
|
|
142
|
+
while (pending.length) {
|
|
143
|
+
const source = pending.pop();
|
|
144
|
+
const target = copies.get(source);
|
|
145
|
+
for (const child of source.children) {
|
|
146
|
+
if (!copies.has(child)) {
|
|
147
|
+
copies.set(child, make(child));
|
|
148
|
+
pending.push(child);
|
|
149
|
+
}
|
|
150
|
+
target.children.push(copies.get(child));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return root;
|
|
107
154
|
}
|
|
108
155
|
|
|
109
156
|
// Resolve a proof forest with an explicit stack: a derivation chain is as deep
|
|
@@ -188,7 +235,8 @@ export function clausesBehindBoundaries(program, roots) {
|
|
|
188
235
|
const goalKey = pending.pop();
|
|
189
236
|
if (seen.has(goalKey)) continue;
|
|
190
237
|
seen.add(goalKey);
|
|
191
|
-
|
|
238
|
+
const candidates = INSPECTION_KEYS.has(goalKey) ? program.inspections.get(goalKey) : byKey.get(goalKey) || [];
|
|
239
|
+
for (const clause of candidates) {
|
|
192
240
|
reached.add(clause.id);
|
|
193
241
|
for (const goal of clause.body) visit(goal);
|
|
194
242
|
}
|
|
@@ -212,7 +260,7 @@ export class Solver {
|
|
|
212
260
|
constructor(program, options) {
|
|
213
261
|
this.program = program;
|
|
214
262
|
this.options = options;
|
|
215
|
-
this.recording = Boolean(options.proof);
|
|
263
|
+
this.recording = Boolean(options.proof || program.inspectionEnabled);
|
|
216
264
|
this.serial = 0;
|
|
217
265
|
this.facts = new Map();
|
|
218
266
|
// Derived facts indexed by argument: for each name and arity, one map
|
|
@@ -220,6 +268,8 @@ export class Solver {
|
|
|
220
268
|
// of the facts with that value there.
|
|
221
269
|
this.factIndex = new Map();
|
|
222
270
|
this.factKeys = new Set(program.groundFactKeys);
|
|
271
|
+
this.genericKeys = new Set();
|
|
272
|
+
this.genericPredicates = new Set();
|
|
223
273
|
this.derived = [];
|
|
224
274
|
this.reported = new Map();
|
|
225
275
|
this.stats = { inferences: 0, rounds: 0, derived: 0 };
|
|
@@ -363,11 +413,18 @@ export class Solver {
|
|
|
363
413
|
}
|
|
364
414
|
if (goalKey === 'findall/3') {
|
|
365
415
|
const items = [];
|
|
416
|
+
const scoped = [];
|
|
366
417
|
const uses = recording ? new Set() : null;
|
|
367
418
|
const mark = env.mark();
|
|
368
419
|
for (const [answerEnv, nodes] of this.solve([goal.args[1]], env, frame.depth + 1)) {
|
|
369
420
|
this.serial++;
|
|
370
|
-
|
|
421
|
+
const names = new Map();
|
|
422
|
+
items.push(freshTerm(copyResolved(goal.args[0], answerEnv), `collection${this.serial}`, names));
|
|
423
|
+
if (this.program.inspectionEnabled) {
|
|
424
|
+
for (const name of scopedVariables(nodes, answerEnv).keys()) {
|
|
425
|
+
if (names.has(name)) scoped.push(names.get(name));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
371
428
|
if (recording) clausesUsed(nodes, uses);
|
|
372
429
|
}
|
|
373
430
|
env.undo(mark);
|
|
@@ -376,10 +433,12 @@ export class Solver {
|
|
|
376
433
|
return null;
|
|
377
434
|
}
|
|
378
435
|
if (!recording) return this.advance(frame, null);
|
|
379
|
-
return this.advance(frame, new Node(goal, COLLECTED, NO_NODES, NO_NODES, uses));
|
|
436
|
+
return this.advance(frame, new Node(goal, COLLECTED, NO_NODES, NO_NODES, uses, false, scoped));
|
|
380
437
|
}
|
|
381
438
|
let point;
|
|
382
|
-
if (goalKey
|
|
439
|
+
if (INSPECTION_KEYS.has(goalKey)) {
|
|
440
|
+
point = new Point('inspection', frame, env.mark(), goal, { alternatives: this.program.inspections.get(goalKey) });
|
|
441
|
+
} else if (goalKey === 'or/2') {
|
|
383
442
|
point = new Point('branch', frame, env.mark(), goal, { alternatives: goal.args });
|
|
384
443
|
} else if (goalKey === 'call/1' || goalKey === 'once/1') {
|
|
385
444
|
const cut = goalKey === 'once/1' ? choices.length : null;
|
|
@@ -409,6 +468,7 @@ export class Solver {
|
|
|
409
468
|
}
|
|
410
469
|
|
|
411
470
|
addFact(goalKey, node) {
|
|
471
|
+
if (node.generic) this.genericPredicates.add(goalKey);
|
|
412
472
|
let facts = this.facts.get(goalKey);
|
|
413
473
|
if (facts === undefined) {
|
|
414
474
|
facts = [];
|
|
@@ -436,6 +496,7 @@ export class Solver {
|
|
|
436
496
|
// The indexes of the derived facts a goal could match, in order, when an
|
|
437
497
|
// argument of the goal narrows them down; null to try them all.
|
|
438
498
|
factOrder(goal, env, goalKey) {
|
|
499
|
+
if (this.genericPredicates.has(goalKey)) return null;
|
|
439
500
|
const positions = this.factIndex.get(goalKey);
|
|
440
501
|
if (positions === undefined) return null;
|
|
441
502
|
let best = null;
|
|
@@ -476,11 +537,21 @@ export class Solver {
|
|
|
476
537
|
return this.childFrame(point.frame, [point.alternatives[position]],
|
|
477
538
|
this.controlPending(point.goal, null), point.frame.depth + 1);
|
|
478
539
|
}
|
|
540
|
+
if (point.kind === 'inspection') {
|
|
541
|
+
const source = point.alternatives[position];
|
|
542
|
+
this.serial++;
|
|
543
|
+
if (unify(point.goal, freshTerm(source.term, this.serial, new Map(), true), env)) {
|
|
544
|
+
return this.advance(point.frame, recording ? new Node(point.goal, new Struct('source', [BigInt(source.id)])) : null);
|
|
545
|
+
}
|
|
546
|
+
env.undo(point.mark);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
479
549
|
const inWindow = point.stop - point.start;
|
|
480
550
|
if (position < inWindow) {
|
|
481
551
|
let index = point.start + position;
|
|
482
552
|
if (point.order !== null) index = point.order[index];
|
|
483
|
-
|
|
553
|
+
let fact = point.facts[index];
|
|
554
|
+
if (fact.generic) fact = freshNode(fact, ++this.serial);
|
|
484
555
|
if (unify(point.goal, fact.goal, env)) {
|
|
485
556
|
if (windowed) this.chosen[point.frame.index] = [0, index];
|
|
486
557
|
return this.advance(point.frame, fact);
|
|
@@ -541,7 +612,7 @@ export class Solver {
|
|
|
541
612
|
break;
|
|
542
613
|
}
|
|
543
614
|
const goalKey = key(goal);
|
|
544
|
-
if (PRIMITIVE_KEYS.has(goalKey) || goalKey === 'not/1' || goalKey === 'findall/3') {
|
|
615
|
+
if (PRIMITIVE_KEYS.has(goalKey) || INSPECTION_KEYS.has(goalKey) || goalKey === 'not/1' || goalKey === 'findall/3') {
|
|
545
616
|
keys.push(null);
|
|
546
617
|
} else if (CONTROL_KEYS.has(goalKey) || (this.program.groups.get(goalKey) || []).some((item) => item.body.length)) {
|
|
547
618
|
keys = null;
|
|
@@ -629,8 +700,24 @@ export class Solver {
|
|
|
629
700
|
// between the heads of one conclusion.
|
|
630
701
|
const resolved = copyResolved(head, answerEnv);
|
|
631
702
|
const unresolved = variables(resolved);
|
|
703
|
+
if (this.program.inspectionEnabled) {
|
|
704
|
+
const scoped = scopedVariables(nodes, answerEnv);
|
|
705
|
+
const captured = clause.captures === null ? null : copyResolved(body[0].args[1], answerEnv);
|
|
706
|
+
const relevant = [...unresolved.keys(), ...(captured === null ? [] : variables(captured).keys())];
|
|
707
|
+
if (relevant.some((name) => scoped.has(name))) {
|
|
708
|
+
const inBody = new Set();
|
|
709
|
+
for (const goal of clause.body) for (const name of variables(goal).keys()) inBody.add(name);
|
|
710
|
+
if ([...variables(clause.head).keys()].some((name) => !inBody.has(name))) {
|
|
711
|
+
throw new EyemindError('bind inspected rule variables before deriving existential witnesses');
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const name of scoped.keys()) unresolved.delete(name);
|
|
715
|
+
}
|
|
632
716
|
if (unresolved.size) {
|
|
633
|
-
|
|
717
|
+
// A generated rule also depends on its captures when its head omits
|
|
718
|
+
// them; otherwise two enabled instances could share a witness.
|
|
719
|
+
const context = clause.captures === null ? null : copyResolved(body[0].args[1], answerEnv);
|
|
720
|
+
const witnesses = skolemTerms(clause, head, unresolved, answerEnv, context);
|
|
634
721
|
[...unresolved.values()].forEach((value, i) => unify(value, witnesses[i], answerEnv));
|
|
635
722
|
}
|
|
636
723
|
const conclusions = unresolved.size
|
|
@@ -674,7 +761,7 @@ export class Solver {
|
|
|
674
761
|
for (const [conclusions, children, bindings, claim] of this.activations(clause)) {
|
|
675
762
|
for (const conclusion of conclusions) {
|
|
676
763
|
if (conclusion === 'true') {
|
|
677
|
-
const claimId = text(claim);
|
|
764
|
+
const claimId = this.program.inspectionEnabled ? variant(claim) : text(claim);
|
|
678
765
|
if (!this.reported.has(claimId)) this.reported.set(claimId, [claim, children, clause.id]);
|
|
679
766
|
continue;
|
|
680
767
|
}
|
|
@@ -684,9 +771,11 @@ export class Solver {
|
|
|
684
771
|
this.haltCode = 65;
|
|
685
772
|
return;
|
|
686
773
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.factKeys
|
|
774
|
+
node.generic = this.program.inspectionEnabled && !isGround(conclusion);
|
|
775
|
+
const conclusionId = node.generic ? variant(conclusion) : identity(conclusion);
|
|
776
|
+
const factKeys = node.generic ? this.genericKeys : this.factKeys;
|
|
777
|
+
if (factKeys.has(conclusionId)) continue;
|
|
778
|
+
factKeys.add(conclusionId);
|
|
690
779
|
this.addFact(key(conclusion), node);
|
|
691
780
|
this.derived.push(node);
|
|
692
781
|
this.stats.derived++;
|
|
@@ -704,8 +793,8 @@ export class Solver {
|
|
|
704
793
|
// variable and the values the solution gave the heads' other variables. The
|
|
705
794
|
// same activation thus always gives the same terms, and a different one
|
|
706
795
|
// different terms.
|
|
707
|
-
export function skolemTerms(clause, head, unresolved, env) {
|
|
708
|
-
const values = [];
|
|
796
|
+
export function skolemTerms(clause, head, unresolved, env, context = null) {
|
|
797
|
+
const values = context === null ? [] : [context];
|
|
709
798
|
for (const variable of variables(head).values()) {
|
|
710
799
|
const value = copyResolved(variable, env);
|
|
711
800
|
if (isGround(value)) values.push(value);
|
|
@@ -741,7 +830,7 @@ export class Result {
|
|
|
741
830
|
|
|
742
831
|
// The derived facts as text, written only when asked for.
|
|
743
832
|
get inferred() {
|
|
744
|
-
return this.derived.map((node) => text(node.goal));
|
|
833
|
+
return this.derived.filter((node) => !isActivation(node.goal)).map((node) => text(node.goal));
|
|
745
834
|
}
|
|
746
835
|
}
|
|
747
836
|
|
|
@@ -808,6 +897,7 @@ function reason(program, goalInputs, options) {
|
|
|
808
897
|
}
|
|
809
898
|
} else {
|
|
810
899
|
for (const node of solver.derived) {
|
|
900
|
+
if (isActivation(node.goal)) continue;
|
|
811
901
|
roots.push(node);
|
|
812
902
|
claims.push(node.goal);
|
|
813
903
|
}
|
package/src/goals.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// An array in a goal position is the conjunction of its items: [p(X), q(X)]
|
|
4
4
|
// is and(p(X), q(X)). A goal position is a statement's goal, an argument of
|
|
5
|
-
// and, or, not, call or once,
|
|
6
|
-
// is a list.
|
|
7
|
-
import { EMPTY, Struct, conjunction, properListItems } from './terms.js';
|
|
5
|
+
// and, or, not, call or once, either part of a rule template, or the goal of
|
|
6
|
+
// findall. Anywhere else an array is a list.
|
|
7
|
+
import { EMPTY, Env, Struct, conjunction, properListItems } from './terms.js';
|
|
8
8
|
import { readTerm } from './reader.js';
|
|
9
9
|
|
|
10
|
-
const EMPTY_ENV =
|
|
10
|
+
const EMPTY_ENV = new Env();
|
|
11
11
|
|
|
12
12
|
function isList(term) {
|
|
13
13
|
return term === EMPTY || (term instanceof Struct && term.name === '.' && term.args.length === 2);
|
|
@@ -25,6 +25,9 @@ export function normalizeGoal(term) {
|
|
|
25
25
|
if ((name === 'and' || name === 'or') && args.length === 2) {
|
|
26
26
|
return rebuild(term, args.map(normalizeGoal));
|
|
27
27
|
}
|
|
28
|
+
if (['implies', 'implied_by', 'forward', 'backward'].includes(name) && args.length === 2) {
|
|
29
|
+
return rebuild(term, args.map(normalizeGoal));
|
|
30
|
+
}
|
|
28
31
|
if ((name === 'not' || name === 'call' || name === 'once') && args.length === 1) {
|
|
29
32
|
return rebuild(term, [normalizeGoal(args[0])]);
|
|
30
33
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Rule inspection reads a fixed source catalogue, not the run's rule index.
|
|
2
|
+
import { Struct, conjunction, isTerm } from './terms.js';
|
|
3
|
+
|
|
4
|
+
export const INSPECTION_KEYS = new Set(['implies/2', 'implied_by/2']);
|
|
5
|
+
export const isInspection = (term) => isTerm(term, 'implies', 2) || isTerm(term, 'implied_by', 2);
|
|
6
|
+
|
|
7
|
+
export function hasInspection(goal) {
|
|
8
|
+
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]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function inspectionTerm(clause) {
|
|
15
|
+
const body = conjunction(clause.body);
|
|
16
|
+
return new Struct(clause.forward ? 'implies' : 'implied_by', clause.forward ? [body, clause.head] : [clause.head, body]);
|
|
17
|
+
}
|
package/src/lang.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// Atoms are strings, integers are numbers with an integral value or bigints,
|
|
11
11
|
// floats are other numbers, lists are arrays, and [H, ...T] is a list with
|
|
12
12
|
// head H and tail T. An array in a goal position is the conjunction of its
|
|
13
|
-
// goals.
|
|
14
|
-
//
|
|
13
|
+
// goals. Rule calls embedded in another statement are templates; other
|
|
14
|
+
// statements contribute clauses in source order. loadText runs the script
|
|
15
|
+
// and collects them into a Program.
|
|
15
16
|
//
|
|
16
17
|
// A name the program does not define is supplied as it runs: a name
|
|
17
18
|
// beginning with an uppercase letter or _ is the variable of that name, and
|
|
@@ -148,10 +149,13 @@ class Builder {
|
|
|
148
149
|
constructor(locate) {
|
|
149
150
|
this.sources = [];
|
|
150
151
|
this.locate = locate;
|
|
152
|
+
this.rules = new WeakMap();
|
|
153
|
+
this.embedded = new Set();
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
add(kind, head, body, line = null) {
|
|
154
157
|
if (line === null) line = statedLine !== null ? statedLine : this.locate();
|
|
158
|
+
const pending = [head, ...body];
|
|
155
159
|
const counter = new Anonymous(head, body);
|
|
156
160
|
// In the order the statement writes them: a forward rule, implies(),
|
|
157
161
|
// writes its premise first.
|
|
@@ -171,7 +175,35 @@ class Builder {
|
|
|
171
175
|
}
|
|
172
176
|
if (goal === EMPTY) throw new EyemindError('an empty array is not a goal');
|
|
173
177
|
}
|
|
174
|
-
|
|
178
|
+
// JavaScript evaluates an inner rule first. Only a valid enclosing
|
|
179
|
+
// statement consumes it, using its value before anonymous renaming.
|
|
180
|
+
while (pending.length) {
|
|
181
|
+
const term = pending.pop();
|
|
182
|
+
if (!(term instanceof Struct)) continue;
|
|
183
|
+
const source = this.rules.get(term);
|
|
184
|
+
if (source !== undefined) this.embedded.add(source);
|
|
185
|
+
pending.push(...term.args);
|
|
186
|
+
}
|
|
187
|
+
const source = new Source(kind, head, body, line);
|
|
188
|
+
this.sources.push(source);
|
|
189
|
+
return source;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
rule(name, args) {
|
|
193
|
+
if (args.length !== 2) throw new EyemindError(`${name} takes 2 arguments, got ${args.length}`);
|
|
194
|
+
const forward = name === 'implies';
|
|
195
|
+
const head = forward ? goalOf(args[1]) : toTerm(args[0]);
|
|
196
|
+
const premise = goalOf(args[forward ? 0 : 1]);
|
|
197
|
+
// Keep the returned value's anonymous variables untouched: each enclosing
|
|
198
|
+
// clause names its own occurrences, including those inside templates.
|
|
199
|
+
const value = new Struct(name, forward ? [premise, head] : [normalizeGoal(head), premise]);
|
|
200
|
+
const source = this.add(forward ? 'forward' : 'backward', head, [premise]);
|
|
201
|
+
this.rules.set(value, source);
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
program() {
|
|
206
|
+
return new Program(this.sources.filter((source) => !this.embedded.has(source)));
|
|
175
207
|
}
|
|
176
208
|
}
|
|
177
209
|
|
|
@@ -254,8 +286,16 @@ function exportsFor(io) {
|
|
|
254
286
|
fact: (...terms) => {
|
|
255
287
|
for (const term of terms) builder.add('fact', toTerm(term), []);
|
|
256
288
|
},
|
|
257
|
-
implies: (
|
|
258
|
-
implied_by: (
|
|
289
|
+
implies: (...args) => active().rule('implies', args),
|
|
290
|
+
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
|
+
},
|
|
259
299
|
query: (...body) => builder.add('query', TRUE_ATOM, body.map(goalOf)),
|
|
260
300
|
contradiction: (...body) => builder.add('contradiction', 'false', body.map(goalOf)),
|
|
261
301
|
facts_from: (source) => {
|
|
@@ -678,7 +718,7 @@ function execute(source, filename, builder, io) {
|
|
|
678
718
|
export function loadText(source, filename = '<program>', io = null) {
|
|
679
719
|
const builder = new Builder(() => null);
|
|
680
720
|
execute(String(source).replace(/^#![^\n]*/, ''), filename, builder, io);
|
|
681
|
-
return
|
|
721
|
+
return builder.program();
|
|
682
722
|
}
|
|
683
723
|
|
|
684
724
|
// One Program from several sources, given as [text, filename] pairs, run in
|
|
@@ -688,7 +728,7 @@ export function loadSources(sources, io = null) {
|
|
|
688
728
|
for (const [source, filename] of sources) {
|
|
689
729
|
execute(String(source).replace(/^#![^\n]*/, ''), filename, builder, io);
|
|
690
730
|
}
|
|
691
|
-
return
|
|
731
|
+
return builder.program();
|
|
692
732
|
}
|
|
693
733
|
|
|
694
734
|
// A Program from a function that states clauses when called: a function of a
|
|
@@ -698,7 +738,7 @@ export function build(statements) {
|
|
|
698
738
|
const outer = builders.length ? builders[builders.length - 1] : null;
|
|
699
739
|
const builder = new Builder(outer ? outer.locate : () => null);
|
|
700
740
|
within(builder, () => statements(exportsFor(null)));
|
|
701
|
-
return
|
|
741
|
+
return builder.program();
|
|
702
742
|
}
|
|
703
743
|
|
|
704
744
|
export { FloatLiteral, PREDICATE };
|
package/src/program.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
// A program: its clauses, validated, indexed and stratified.
|
|
2
2
|
import { PRIMITIVE_KEYS } from './builtins.js';
|
|
3
|
+
import { ACTIVATION_KEY, containsTemplate, expandTemplates } from './templates.js';
|
|
4
|
+
import { hasInspection, inspectionTerm } from './inspection.js';
|
|
3
5
|
import {
|
|
4
6
|
Env, EyemindError, Struct, Var, deref, flattenConjunction, freshTerm, identity, isCallable,
|
|
5
7
|
isGround, isTerm, key, keyArity, unify,
|
|
6
8
|
} from './terms.js';
|
|
7
9
|
|
|
8
10
|
export const CONTROL_KEYS = new Set(['and/2', 'or/2', 'not/1', 'call/1', 'once/1', 'findall/3']);
|
|
9
|
-
export const RESERVED_KEYS = new Set(['step/4', 'clause/2']);
|
|
11
|
+
export const RESERVED_KEYS = new Set(['step/4', 'clause/2', ACTIVATION_KEY, 'implies/2', 'implied_by/2', 'forward/2', 'backward/2']);
|
|
10
12
|
|
|
11
13
|
// The statements that state a forward rule, and how an error names them.
|
|
12
14
|
const FORWARD_KINDS = { forward: 'a forward rule', query: 'query()', contradiction: 'contradiction()' };
|
|
13
15
|
const isIndexable = (value) => typeof value === 'string' || typeof value === 'bigint' || typeof value === 'number';
|
|
14
16
|
|
|
15
17
|
export class Clause {
|
|
16
|
-
constructor(id, head, heads, body, forward, line) {
|
|
18
|
+
constructor(id, head, heads, body, forward, line, captures = null) {
|
|
17
19
|
this.id = id;
|
|
18
20
|
this.head = head;
|
|
19
21
|
// Only a forward rule can have several heads, so only it stores them.
|
|
@@ -21,6 +23,7 @@ export class Clause {
|
|
|
21
23
|
this.body = body;
|
|
22
24
|
this.forward = forward;
|
|
23
25
|
this.line = line;
|
|
26
|
+
this.captures = captures;
|
|
24
27
|
this.renaming = null; // how to rename it apart, worked out once it is used often
|
|
25
28
|
this.uses = 0;
|
|
26
29
|
}
|
|
@@ -68,6 +71,8 @@ function pushGoals(goal, out) {
|
|
|
68
71
|
export class Program {
|
|
69
72
|
constructor(sources, without = null) {
|
|
70
73
|
this.sources = [...sources];
|
|
74
|
+
const expanded = expandTemplates(this.sources);
|
|
75
|
+
this.maxClauseId = expanded.length;
|
|
71
76
|
this.clauses = [];
|
|
72
77
|
this.byId = new Map();
|
|
73
78
|
this.groups = new Map();
|
|
@@ -77,9 +82,9 @@ export class Program {
|
|
|
77
82
|
// made once, and a key once allowed as a head is not checked again.
|
|
78
83
|
const keys = new KeyCache();
|
|
79
84
|
const allowed = new Set();
|
|
80
|
-
const count =
|
|
85
|
+
const count = expanded.length;
|
|
81
86
|
for (let index = 0; index < count; index++) {
|
|
82
|
-
const source =
|
|
87
|
+
const source = expanded[index];
|
|
83
88
|
const ordinal = index + 1;
|
|
84
89
|
if (ordinal === without) continue;
|
|
85
90
|
const forward = Object.hasOwn(FORWARD_KINDS, source.kind);
|
|
@@ -104,14 +109,14 @@ export class Program {
|
|
|
104
109
|
const itemKey = keys.of(item);
|
|
105
110
|
headKey = itemKey;
|
|
106
111
|
if (allowed.has(itemKey)) continue;
|
|
107
|
-
if (RESERVED_KEYS.has(itemKey) ||
|
|
112
|
+
if ((RESERVED_KEYS.has(itemKey) && itemKey !== ACTIVATION_KEY) ||
|
|
108
113
|
((PRIMITIVE_KEYS.has(itemKey) || CONTROL_KEYS.has(itemKey)) &&
|
|
109
114
|
!(forward && (item === 'true' || item === 'false')))) {
|
|
110
115
|
throw new EyemindError(`line ${source.line}: unsupported or reserved head ${itemKey}`);
|
|
111
116
|
}
|
|
112
117
|
allowed.add(itemKey);
|
|
113
118
|
}
|
|
114
|
-
const clause = new Clause(ordinal, source.head, forward ? heads : null, body, forward, source.line);
|
|
119
|
+
const clause = new Clause(ordinal, source.head, forward ? heads : null, body, forward, source.line, source.captures ?? null);
|
|
115
120
|
this.clauses.push(clause);
|
|
116
121
|
this.byId.set(ordinal, clause);
|
|
117
122
|
if (forward) {
|
|
@@ -127,6 +132,8 @@ export class Program {
|
|
|
127
132
|
// clauses: many groups of a large program are never asked for, and a
|
|
128
133
|
// program made to leave one clause out is often run only briefly.
|
|
129
134
|
this.indexes = new Map();
|
|
135
|
+
this.inspectionEnabled = this.clauses.some((clause) => clause.body.some(hasInspection));
|
|
136
|
+
this.inspectionCatalogue = null;
|
|
130
137
|
// Ground source facts are compared against forward conclusions by
|
|
131
138
|
// identity, so work those out once here.
|
|
132
139
|
this.groundFactKeys = new Set();
|
|
@@ -136,6 +143,31 @@ export class Program {
|
|
|
136
143
|
this.strata = stratify(this.clauses, stratifying);
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
prepareInspections() {
|
|
147
|
+
if (this.inspectionCatalogue !== null) return;
|
|
148
|
+
this.inspectionCatalogue = new Map([['implies/2', []], ['implied_by/2', []]]);
|
|
149
|
+
this.inspectionSources = new Map();
|
|
150
|
+
for (const clause of this.clauses) {
|
|
151
|
+
const source = this.sources[clause.id - 1];
|
|
152
|
+
if (!source || !['forward', 'backward'].includes(source.kind) ||
|
|
153
|
+
containsTemplate(source.head) || source.body.some(hasInspection)) continue;
|
|
154
|
+
const term = inspectionTerm(clause);
|
|
155
|
+
const entry = { id: clause.id, term, body: [] };
|
|
156
|
+
this.inspectionCatalogue.get(key(term)).push(entry);
|
|
157
|
+
this.inspectionSources.set(clause.id, entry);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get inspections() {
|
|
162
|
+
this.prepareInspections();
|
|
163
|
+
return this.inspectionCatalogue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get inspectedById() {
|
|
167
|
+
this.prepareInspections();
|
|
168
|
+
return this.inspectionSources;
|
|
169
|
+
}
|
|
170
|
+
|
|
139
171
|
candidates(goal, env, goalKey) {
|
|
140
172
|
const clauses = this.groups.get(goalKey);
|
|
141
173
|
if (clauses === undefined) return [];
|
package/src/proof.js
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
import { PRIMITIVE_KEYS, primitive } from './builtins.js';
|
|
12
12
|
import { freshClause, text } from './common.js';
|
|
13
13
|
import { readGoal } from './goals.js';
|
|
14
|
+
import { INSPECTION_KEYS, isInspection } from './inspection.js';
|
|
14
15
|
import { CONTROL_KEYS, Program } from './program.js';
|
|
15
16
|
import { readTerms } from './reader.js';
|
|
17
|
+
import { isActivation } from './templates.js';
|
|
16
18
|
import {
|
|
17
19
|
Env, EyemindError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
|
|
18
20
|
isCallable, isGround, isTerm, key, listFromItems, properListItems, unify, variables,
|
|
@@ -215,6 +217,7 @@ function check(program, claims, steps, goals, allowTrusted, fail, spellings, nam
|
|
|
215
217
|
valid: !fail.items.length,
|
|
216
218
|
steps: steps.size,
|
|
217
219
|
claims: claims.length,
|
|
220
|
+
answers: claims.map((claim) => name(claim)),
|
|
218
221
|
verified: tally.verified,
|
|
219
222
|
redecided: tally.redecided,
|
|
220
223
|
composed: tally.composed,
|
|
@@ -284,7 +287,7 @@ function readDocument(program, document, fail, spellings) {
|
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
function clauseNamed(program, id) {
|
|
287
|
-
if (typeof id !== 'bigint' || id < 1n || id > BigInt(program.
|
|
290
|
+
if (typeof id !== 'bigint' || id < 1n || id > BigInt(program.maxClauseId)) return null;
|
|
288
291
|
return program.byId.get(Number(id)) ?? null;
|
|
289
292
|
}
|
|
290
293
|
|
|
@@ -318,6 +321,15 @@ function checkSteps(program, steps, allowTrusted, fail, spellings, name) {
|
|
|
318
321
|
}
|
|
319
322
|
if (isTerm(by, 'clause', 1)) {
|
|
320
323
|
if (checkResolution(program, step, fail, tally)) tally.verified++;
|
|
324
|
+
} else if (isTerm(by, 'source', 1)) {
|
|
325
|
+
const clause = clauseNamed(program, by.args[0]);
|
|
326
|
+
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
|
+
if (!inspected || bindings.length || uses.length || !isInspection(goal) ||
|
|
330
|
+
!unify(pattern, goal, env) || !resolvesTo(pattern, env, goal)) {
|
|
331
|
+
fail.add('C1', `not an instance of inspectable source rule ${text(by.args[0])}: ${text(goal)}`, goal);
|
|
332
|
+
} else tally.verified++;
|
|
321
333
|
} else if (by === BUILTIN_BY) {
|
|
322
334
|
if (bindings.length || uses.length || !isCallable(goal) || !PRIMITIVE_KEYS.has(key(goal))) {
|
|
323
335
|
fail.add('C3', 'invalid builtin justification', goal);
|
|
@@ -491,6 +503,9 @@ function witnessNames(clause) {
|
|
|
491
503
|
function checkWitnesses(clause, head, names, env) {
|
|
492
504
|
const witnessed = witnessNames(clause);
|
|
493
505
|
if (!witnessed.length) return null;
|
|
506
|
+
// An existential firing needs bound inspection values. A generic rule
|
|
507
|
+
// pattern cannot silently share one witness across all its instances.
|
|
508
|
+
if (!isGround(copyResolved(head, env))) return false;
|
|
494
509
|
let firing = null;
|
|
495
510
|
for (const name of witnessed) {
|
|
496
511
|
const value = copyResolved(names.get(name), env);
|
|
@@ -509,7 +524,11 @@ function checkWitnesses(clause, head, names, env) {
|
|
|
509
524
|
}
|
|
510
525
|
return false;
|
|
511
526
|
};
|
|
512
|
-
|
|
527
|
+
// Recover captures from the source clause and substitution, independently
|
|
528
|
+
// of the proposed witness, including captures the head does not mention.
|
|
529
|
+
const expected = clause.captures === null ? [] : [listFromItems(
|
|
530
|
+
clause.captures.map((name) => copyResolved(names.get(name), env)),
|
|
531
|
+
)];
|
|
513
532
|
for (const variable of variables(head).values()) {
|
|
514
533
|
const value = copyResolved(variable, env);
|
|
515
534
|
if (isGround(value) && !ofThisFiring(value)) expected.push(value);
|
|
@@ -558,6 +577,9 @@ function checkBoundaries(program, steps, boundaries, fail) {
|
|
|
558
577
|
return [];
|
|
559
578
|
}
|
|
560
579
|
if (CONTROL_KEYS.has(goalKey)) return null;
|
|
580
|
+
if (INSPECTION_KEYS.has(goalKey)) {
|
|
581
|
+
return program.inspections.get(goalKey).map((entry) => freshTerm(entry.term, `evidence${nextSerial()}`));
|
|
582
|
+
}
|
|
561
583
|
const facts = (program.groups.get(goalKey) || [])
|
|
562
584
|
.filter((clause) => !clause.body.length && !clause.forward)
|
|
563
585
|
.map((clause) => freshTerm(clause.head, `evidence${nextSerial()}`));
|
|
@@ -625,7 +647,7 @@ function checkRelevance(program, claims, steps, goals, fail, spellings) {
|
|
|
625
647
|
if (heads.some((head) => head === 'true')) {
|
|
626
648
|
if (!halted && clause.body.length) questions.push(conjunction(clause.body));
|
|
627
649
|
}
|
|
628
|
-
for (const head of heads) if (head !== 'true') questions.push(head);
|
|
650
|
+
for (const head of heads) if (head !== 'true' && !isActivation(head)) questions.push(head);
|
|
629
651
|
}
|
|
630
652
|
}
|
|
631
653
|
for (const claim of claims) {
|
|
@@ -727,7 +749,27 @@ export function checkReport(report) {
|
|
|
727
749
|
facts.push(new Struct(name, [BigInt(count)]));
|
|
728
750
|
}
|
|
729
751
|
facts.push(new Struct('verdict', [verdict(report)]));
|
|
730
|
-
|
|
752
|
+
const heading = !report.valid ? '// Unverified claims: proof checking failed.'
|
|
753
|
+
: !report.answers.length ? '// No answers claimed; this check establishes no conclusions.'
|
|
754
|
+
: report.trusted.length ? '// Answers checked subject to the obligations below.'
|
|
755
|
+
: '// Checked answers: these follow from the input program.';
|
|
756
|
+
const lines = [heading, ...report.answers, '',
|
|
757
|
+
'// C1-C8: resolution, acyclicity, justification, coverage,',
|
|
758
|
+
'// re-decision, boundary consistency, relevance and witness integrity.'];
|
|
759
|
+
let previous = null;
|
|
760
|
+
for (const fact of facts) {
|
|
761
|
+
if (fact.name === 'failure' && previous !== 'failure') {
|
|
762
|
+
lines.push('', '// Failures: the claims above have not passed every check.');
|
|
763
|
+
}
|
|
764
|
+
if (fact.name === 'obligation' && previous !== 'obligation') {
|
|
765
|
+
lines.push('', '// Obligations: absence and collection completeness are taken on trust.');
|
|
766
|
+
}
|
|
767
|
+
if (fact.name === 'steps') lines.push('', '// Proof statistics: recorded steps and how they were checked.');
|
|
768
|
+
if (fact.name === 'verdict') lines.push('', '// This verdict applies to the claims above; it does not establish completeness.');
|
|
769
|
+
lines.push(text(lettered(fact)));
|
|
770
|
+
previous = fact.name;
|
|
771
|
+
}
|
|
772
|
+
return `${lines.join('\n')}\n`;
|
|
731
773
|
}
|
|
732
774
|
|
|
733
775
|
export function verdictText(report) {
|
package/src/templates.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Inline rule templates become statically visible clauses whose first
|
|
2
|
+
// premise records the generating activation. No rule is installed at run time.
|
|
3
|
+
import { normalizeGoal } from './goals.js';
|
|
4
|
+
import { isInspection } from './inspection.js';
|
|
5
|
+
import {
|
|
6
|
+
EMPTY, EyemindError, Struct, Var, conjunction, flattenConjunction, isCallable, isTerm, listFromItems, variables,
|
|
7
|
+
} from './terms.js';
|
|
8
|
+
|
|
9
|
+
export const ACTIVATION_KEY = 'activated/2';
|
|
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);
|
|
13
|
+
|
|
14
|
+
export function containsTemplate(term) {
|
|
15
|
+
const pending = [term];
|
|
16
|
+
while (pending.length) {
|
|
17
|
+
const item = pending.pop();
|
|
18
|
+
if (isTemplate(item)) return true;
|
|
19
|
+
if (item instanceof Struct) pending.push(...item.args);
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function checkGoal(goal, line, generated = false) {
|
|
25
|
+
if (generated && goal instanceof Var) throw new EyemindError(`line ${line}: generated rule bodies require statically named calls`);
|
|
26
|
+
if (generated && (!isCallable(goal) || goal === EMPTY || isTerm(goal, '.', 2))) {
|
|
27
|
+
throw new EyemindError(`line ${line}: a generated rule goal must be an atom or a compound, not a number or list`);
|
|
28
|
+
}
|
|
29
|
+
if (!(goal instanceof Struct)) return;
|
|
30
|
+
if (isInspection(goal)) return;
|
|
31
|
+
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);
|
|
41
|
+
} else if (containsTemplate(goal)) {
|
|
42
|
+
throw new EyemindError(`line ${line}: rule templates must be top-level conjuncts of an implies() conclusion or inspection goals`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expandTemplates(sources) {
|
|
47
|
+
const expanded = [...sources];
|
|
48
|
+
for (let index = 0; index < expanded.length; index++) {
|
|
49
|
+
const source = expanded[index];
|
|
50
|
+
for (const goal of source.body) checkGoal(goal, source.line, source.captures !== undefined);
|
|
51
|
+
const heads = flattenConjunction(source.head);
|
|
52
|
+
for (const head of heads) {
|
|
53
|
+
if (isActivation(head)) throw new EyemindError(`line ${source.line}: activated/2 is reserved for generated rules`);
|
|
54
|
+
}
|
|
55
|
+
const templates = heads.filter(isTemplate);
|
|
56
|
+
if (!templates.length) {
|
|
57
|
+
if (containsTemplate(source.head)) {
|
|
58
|
+
throw new EyemindError(`line ${source.line}: rule templates must be top-level conjuncts of an implies() conclusion`);
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (source.kind !== 'forward') {
|
|
63
|
+
throw new EyemindError(`line ${source.line}: rule templates belong in an implies() conclusion`);
|
|
64
|
+
}
|
|
65
|
+
// Variables occurring outside a template are captured. Other variables
|
|
66
|
+
// belong to that generated rule and are freshened by ordinary resolution.
|
|
67
|
+
const outer = new Map();
|
|
68
|
+
for (const goal of source.body) variables(goal, outer);
|
|
69
|
+
for (const head of heads) if (!isTemplate(head)) variables(head, outer);
|
|
70
|
+
const lowered = heads.map((head) => {
|
|
71
|
+
if (!isTemplate(head)) {
|
|
72
|
+
if (containsTemplate(head)) throw new EyemindError(`line ${source.line}: a rule template cannot be nested inside an ordinary term`);
|
|
73
|
+
return head;
|
|
74
|
+
}
|
|
75
|
+
const forward = head.name === 'implies' || head.name === 'forward';
|
|
76
|
+
const premise = normalizeGoal(head.args[forward ? 0 : 1]);
|
|
77
|
+
const conclusion = normalizeGoal(head.args[forward ? 1 : 0]);
|
|
78
|
+
for (const item of flattenConjunction(conclusion)) {
|
|
79
|
+
if (item === EMPTY || isTerm(item, '.', 2)) {
|
|
80
|
+
throw new EyemindError(`line ${source.line}: a generated rule conclusion cannot be an empty or open list`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const body = flattenConjunction(premise);
|
|
84
|
+
for (const goal of body) checkGoal(goal, source.line, true);
|
|
85
|
+
const captured = [...variables(head).values()].filter((variable) => outer.has(variable.name));
|
|
86
|
+
const activation = new Struct('activated', [BigInt(expanded.length + 1), listFromItems(captured)]);
|
|
87
|
+
const conclusions = forward ? [conclusion] : flattenConjunction(conclusion);
|
|
88
|
+
for (const generated of conclusions) {
|
|
89
|
+
expanded.push({
|
|
90
|
+
kind: forward ? 'forward' : 'backward', head: generated, body: [activation, ...body],
|
|
91
|
+
line: source.line, file: source.file, captures: captured.map((variable) => variable.name),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return activation;
|
|
95
|
+
});
|
|
96
|
+
expanded[index] = { ...source, head: conjunction(lowered) };
|
|
97
|
+
}
|
|
98
|
+
return expanded;
|
|
99
|
+
}
|
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.27';
|