eyemind 0.1.26 → 0.1.28
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 +2 -2
- package/SPEC.md +91 -11
- package/package.json +1 -1
- package/src/engine.js +120 -23
- package/src/goals.js +1 -1
- package/src/inspection.js +17 -0
- package/src/lang.js +40 -8
- package/src/program.js +30 -2
- package/src/proof.js +16 -0
- package/src/templates.js +22 -14
- package/src/version.js +1 -1
- package/src/writer.js +28 -0
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
|
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.28
|
|
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.28, 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.
|
|
@@ -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
|
|
|
@@ -262,13 +269,29 @@ name MAY have different ranks.
|
|
|
262
269
|
|
|
263
270
|
### 4.6 Inline rule templates
|
|
264
271
|
|
|
265
|
-
`
|
|
266
|
-
|
|
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
|
|
267
276
|
conclusion MAY combine ordinary heads and several templates. Each template's
|
|
268
277
|
body and head MAY themselves be conjunctions, written as arrays. A backward
|
|
269
278
|
template with several heads defines one backward clause per head, sharing
|
|
270
279
|
the same activation and body. Empty and open lists are not goals or heads.
|
|
271
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
|
+
|
|
272
295
|
A template variable is **captured** when it also occurs in the generating
|
|
273
296
|
clause's body or in an ordinary sibling head. The other variables are local
|
|
274
297
|
to that template: they are universally quantified within the generated rule,
|
|
@@ -293,7 +316,7 @@ are numbered consecutively after them and inherit the generator's source line.
|
|
|
293
316
|
Capture metadata belongs to each appended clause, including one whose head
|
|
294
317
|
is subsequently lowered again.
|
|
295
318
|
|
|
296
|
-
For example, `implies(cat(X),
|
|
319
|
+
For example, `implies(cat(X), implies(dog(Y), test('yes')))` as the only
|
|
297
320
|
stated clause becomes these two clauses:
|
|
298
321
|
|
|
299
322
|
```js
|
|
@@ -309,8 +332,44 @@ activation dependencies participate in Section 4.5, including clauses whose
|
|
|
309
332
|
generators never fire. A closed dependency cycle MUST therefore be rejected
|
|
310
333
|
even if its template would remain inactive.
|
|
311
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
|
+
A rule value used as an inspection pattern is embedded in the inspecting
|
|
344
|
+
statement (Section 4.6), so a stored rule value used as a pattern loses its
|
|
345
|
+
standalone clause and is not in the catalogue.
|
|
346
|
+
|
|
347
|
+
The catalogue keeps source order. A rule's body and forward head conjunctions
|
|
348
|
+
are terms `and(...)`, as normalized goals are, and inspection arguments are
|
|
349
|
+
goal positions. Each attempt freshens the whole rule with one variable map:
|
|
350
|
+
shared variables stay shared, and different attempts are independent. Matching
|
|
351
|
+
can instantiate these variables. Inspection reads the rule's structure; it
|
|
352
|
+
does not solve its body or establish that its head holds.
|
|
353
|
+
|
|
354
|
+
Free variables supplied by a successful inspection are scoped universal
|
|
355
|
+
pattern variables. Forward conclusions preserve them, including through
|
|
356
|
+
backward derivations and copied collection results. Derived patterns are
|
|
357
|
+
identified up to variable renaming and freshened, together with their evidence,
|
|
358
|
+
at every use. Unrelated unbound variables keep Section 7.2's witness behavior.
|
|
359
|
+
When inspection variables remain free in a conclusion or its captured
|
|
360
|
+
template context, that firing MUST NOT introduce head-only existential
|
|
361
|
+
witnesses: it MUST fail with an error requiring the inspection variables to
|
|
362
|
+
be bound first. Section 11.9 rejects certificates that bypass this boundary.
|
|
363
|
+
|
|
364
|
+
Inspection uses a fixed source catalogue, so its own results do not depend on
|
|
365
|
+
forward rounds. Other goals using derived patterns still participate in
|
|
366
|
+
stratification. Negation and collection retain their explicit obligations.
|
|
367
|
+
|
|
312
368
|
## 5. Goals, Controls and Primitives
|
|
313
369
|
|
|
370
|
+
The program-dependent source inspection goals `implies/2` and `implied_by/2`
|
|
371
|
+
are defined in Section 4.7.
|
|
372
|
+
|
|
314
373
|
### 5.1 Controls
|
|
315
374
|
|
|
316
375
|
| Control | Term | Meaning |
|
|
@@ -455,7 +514,8 @@ concluded:
|
|
|
455
514
|
shared. A variable that a value of the solution brought in, rather than a
|
|
456
515
|
head variable, is named after its own variable, followed by `1`, `2`, ...
|
|
457
516
|
when that name is taken. A rule that keeps meeting new activations does
|
|
458
|
-
not terminate (Section 7.3). Derived facts
|
|
517
|
+
not terminate (Section 7.3). Derived facts other than scoped inspection
|
|
518
|
+
patterns (Section 4.7) are therefore ground. A
|
|
459
519
|
checker verifies the Skolem terms of a proof (C8, Section 11.9);
|
|
460
520
|
- a head `'true'` reports the instance of the rule's body, as a conjunction,
|
|
461
521
|
once per distinct instance (by identity of canonical text);
|
|
@@ -473,6 +533,12 @@ witnesses of different captured contexts even when a head does not mention
|
|
|
473
533
|
those values. An empty capture list still contributes one element, `[]`.
|
|
474
534
|
Original clauses, including generators, use the ordinary witness convention.
|
|
475
535
|
|
|
536
|
+
Scoped inspection variables of Section 4.7 are preserved instead of receiving
|
|
537
|
+
witnesses; derived conclusions may therefore be universal patterns. Each use
|
|
538
|
+
renames a pattern and its evidence apart. Such patterns are suppressed when
|
|
539
|
+
an alpha-equivalent pattern was already derived. Other derived facts keep
|
|
540
|
+
their ordinary ground identity convention.
|
|
541
|
+
|
|
476
542
|
### 7.3 Bounds
|
|
477
543
|
|
|
478
544
|
A run is bounded by `max_depth`, the depth of nested rule bodies (default
|
|
@@ -658,6 +724,7 @@ order. `By` is one of:
|
|
|
658
724
|
| `By` | Meaning | `Bindings` | `Uses` |
|
|
659
725
|
| --- | --- | --- | --- |
|
|
660
726
|
| `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 |
|
|
727
|
+
| `source(N)` | `Goal` is an instance of the inspectable source rule `N` as `implies(Body, Head)` or `implied_by(Head, Body)`. | empty | empty |
|
|
661
728
|
| `'builtin'` | `Goal` is a primitive that holds. | empty | empty |
|
|
662
729
|
| `'control'` | `Goal` is `call`, `once` or a disjunction, solved by `Uses`. | empty | the solved goals |
|
|
663
730
|
| `'absent'` | `Goal` is a negation taken on trust. | empty | empty |
|
|
@@ -700,6 +767,14 @@ that afterwards the head is identical to `Goal` and each body goal to its
|
|
|
700
767
|
use. A step's own terms MUST NOT need further instantiation to match: a
|
|
701
768
|
source clause `same(X, X)` cannot justify `same('a', 'b')`.
|
|
702
769
|
|
|
770
|
+
For a `source(N)` step, `N` MUST identify an eligible source rule of Section
|
|
771
|
+
4.7. Bindings and uses MUST both be empty. The rule's inspection term,
|
|
772
|
+
freshened with one variable map, MUST unify with `Goal` and resolve exactly
|
|
773
|
+
to it without instantiating the step's own terms. This checks variable
|
|
774
|
+
sharing as well as the rule's direction and structure. The source rule is
|
|
775
|
+
read as data; its body is not derived by the checker. These steps count as
|
|
776
|
+
verified C1 steps and cite the usual source clause display.
|
|
777
|
+
|
|
703
778
|
### 11.3 C2 Well-foundedness
|
|
704
779
|
|
|
705
780
|
No step MAY depend on itself through the uses of steps.
|
|
@@ -768,6 +843,10 @@ bound to `skolem(N, Name, Args)`, where:
|
|
|
768
843
|
the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
|
|
769
844
|
the values that do contain one hold a witness of this firing.
|
|
770
845
|
|
|
846
|
+
When a forward clause has any such variables, its full head under the step's
|
|
847
|
+
substitution MUST be ground. A generic inspection pattern cannot share one
|
|
848
|
+
existential witness across all its instances (Section 4.7).
|
|
849
|
+
|
|
771
850
|
For an appended forward clause of Section 4.6, prepend the capture list,
|
|
772
851
|
instantiated under the step's substitution, as one element of `Args`, as
|
|
773
852
|
required by Section 7.2. The checker MUST compute this from the source
|
|
@@ -782,7 +861,7 @@ covered by C8.
|
|
|
782
861
|
|
|
783
862
|
| Condition | Covered |
|
|
784
863
|
| --- | --- |
|
|
785
|
-
| C1 resolution | `clause` steps that passed C1 |
|
|
864
|
+
| C1 resolution | `clause` and `source` steps that passed C1 |
|
|
786
865
|
| C2 well_founded | steps |
|
|
787
866
|
| C3 justification | steps |
|
|
788
867
|
| C4 coverage | claims plus uses |
|
|
@@ -792,7 +871,8 @@ covered by C8.
|
|
|
792
871
|
| C8 witness_integrity | `clause` steps of forward rules with witnesses that passed C8 |
|
|
793
872
|
|
|
794
873
|
A check also counts the **steps** it read, the **claims**, the `clause` steps
|
|
795
|
-
**verified** by C1, the `
|
|
874
|
+
**verified** by C1, the `source` inspection steps also **verified** by C1,
|
|
875
|
+
the `'builtin'` steps **recomputed** by C5, the
|
|
796
876
|
`'control'` steps **composed** by C5, and the `'absent'` and `'collected'`
|
|
797
877
|
steps **trusted**.
|
|
798
878
|
|
|
@@ -846,7 +926,7 @@ it differently. eyemind's details are, by condition:
|
|
|
846
926
|
|
|
847
927
|
| Condition | Details |
|
|
848
928
|
| --- | --- |
|
|
849
|
-
| C1 | `clause display differs from source`, `unknown clause ...`, `not an instance of source clause N: ...` |
|
|
929
|
+
| C1 | `clause display differs from source`, `unknown clause ...`, `not an instance of source clause N: ...`, `not an instance of inspectable source rule N: ...` |
|
|
850
930
|
| C2 | `cyclic derivation at ...` |
|
|
851
931
|
| 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 ...` |
|
|
852
932
|
| C4 | `unjustified claim ...`, `unjustified use ...` |
|
|
@@ -957,7 +1037,7 @@ conclusions and proof documents in the canonical text, and checks every proof
|
|
|
957
1037
|
it produces as Section 11 describes. A conforming **checker** implements
|
|
958
1038
|
Sections 9, 11 and 12 and does not depend on a reasoner. For the same
|
|
959
1039
|
program, a conforming reasoner and checker MUST produce the same
|
|
960
|
-
conclusions, proof documents and reports as eyemind 0.1.
|
|
1040
|
+
conclusions, proof documents and reports as eyemind 0.1.28, byte for byte,
|
|
961
1041
|
except for comments and the `Detail` texts of failures (Section 12), the internal names of
|
|
962
1042
|
variables where Section 3 says so, and where the host's floating-point
|
|
963
1043
|
library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
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';
|
|
7
8
|
import { isActivation } from './templates.js';
|
|
8
9
|
import {
|
|
9
10
|
Env, EyemindError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
|
|
10
|
-
identity, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems,
|
|
11
|
-
variables,
|
|
11
|
+
identity, isAnonymous, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems,
|
|
12
|
+
unify, variables,
|
|
12
13
|
} from './terms.js';
|
|
13
|
-
import { Lettering, write } from './writer.js';
|
|
14
|
+
import { Lettering, SourceNaming, write } from './writer.js';
|
|
14
15
|
|
|
15
16
|
export const BUILTIN = 'builtin';
|
|
16
17
|
export const CONTROL = 'control';
|
|
@@ -19,7 +20,7 @@ export const COLLECTED = 'collected';
|
|
|
19
20
|
|
|
20
21
|
// One proof step: a goal, how it was justified, and the steps it used.
|
|
21
22
|
export class Node {
|
|
22
|
-
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) {
|
|
23
24
|
this.goal = goal;
|
|
24
25
|
// A derived fact's node is resolved once and for all when the fact is
|
|
25
26
|
// derived, so whatever uses the fact shares the node, derivation and all.
|
|
@@ -30,6 +31,8 @@ export class Node {
|
|
|
30
31
|
// A collection has no children in a proof, so its node keeps the clauses
|
|
31
32
|
// its answers used.
|
|
32
33
|
this.collectedUses = collectedUses;
|
|
34
|
+
this.scoped = scoped;
|
|
35
|
+
this.generic = false;
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -92,7 +95,7 @@ class Point {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
count() {
|
|
95
|
-
if (this.kind === 'branch') return this.alternatives.length;
|
|
98
|
+
if (this.kind === 'branch' || this.kind === 'inspection') return this.alternatives.length;
|
|
96
99
|
return this.stop - this.start + this.clauses.length;
|
|
97
100
|
}
|
|
98
101
|
}
|
|
@@ -104,7 +107,62 @@ const SPLICE = 'splice';
|
|
|
104
107
|
function resolveOne(node, env) {
|
|
105
108
|
return new Node(copyResolved(node.goal, env), node.by,
|
|
106
109
|
node.bindings.map(([name, value]) => [name, copyResolved(value, env)]),
|
|
107
|
-
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;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The error for a negation whose goal still has a variable to bind, naming
|
|
157
|
+
// the variables as the program wrote them rather than as renamed apart.
|
|
158
|
+
function unboundNegation(goal, env) {
|
|
159
|
+
const naming = new SourceNaming();
|
|
160
|
+
const resolved = copyResolved(goal, env);
|
|
161
|
+
const shown = write(resolved, null, naming);
|
|
162
|
+
const unbound = [...new Set([...variables(resolved).values()]
|
|
163
|
+
.filter((variable) => !isAnonymous(variable)).map((variable) => naming.get(variable.name)))];
|
|
164
|
+
return new EyemindError('negation requires a goal whose variables are bound, except for anonymous ones: ' +
|
|
165
|
+
`${unbound.join(', ')} ${unbound.length === 1 ? 'is' : 'are'} unbound in ${shown}`);
|
|
108
166
|
}
|
|
109
167
|
|
|
110
168
|
// Resolve a proof forest with an explicit stack: a derivation chain is as deep
|
|
@@ -189,7 +247,8 @@ export function clausesBehindBoundaries(program, roots) {
|
|
|
189
247
|
const goalKey = pending.pop();
|
|
190
248
|
if (seen.has(goalKey)) continue;
|
|
191
249
|
seen.add(goalKey);
|
|
192
|
-
|
|
250
|
+
const candidates = INSPECTION_KEYS.has(goalKey) ? program.inspections.get(goalKey) : byKey.get(goalKey) || [];
|
|
251
|
+
for (const clause of candidates) {
|
|
193
252
|
reached.add(clause.id);
|
|
194
253
|
for (const goal of clause.body) visit(goal);
|
|
195
254
|
}
|
|
@@ -213,7 +272,7 @@ export class Solver {
|
|
|
213
272
|
constructor(program, options) {
|
|
214
273
|
this.program = program;
|
|
215
274
|
this.options = options;
|
|
216
|
-
this.recording = Boolean(options.proof);
|
|
275
|
+
this.recording = Boolean(options.proof || program.inspectionEnabled);
|
|
217
276
|
this.serial = 0;
|
|
218
277
|
this.facts = new Map();
|
|
219
278
|
// Derived facts indexed by argument: for each name and arity, one map
|
|
@@ -221,6 +280,8 @@ export class Solver {
|
|
|
221
280
|
// of the facts with that value there.
|
|
222
281
|
this.factIndex = new Map();
|
|
223
282
|
this.factKeys = new Set(program.groundFactKeys);
|
|
283
|
+
this.genericKeys = new Set();
|
|
284
|
+
this.genericPredicates = new Set();
|
|
224
285
|
this.derived = [];
|
|
225
286
|
this.reported = new Map();
|
|
226
287
|
this.stats = { inferences: 0, rounds: 0, derived: 0 };
|
|
@@ -346,10 +407,7 @@ export class Solver {
|
|
|
346
407
|
if (goalKey === 'not/1') {
|
|
347
408
|
// Negation is a test, not a way to bind its variables; its anonymous
|
|
348
409
|
// variables stand for any value.
|
|
349
|
-
if (!isGroundButAnonymous(goal.args[0], env))
|
|
350
|
-
throw new EyemindError('negation requires a goal whose variables are bound, ' +
|
|
351
|
-
`except for anonymous ones: ${text(goal, env)}`);
|
|
352
|
-
}
|
|
410
|
+
if (!isGroundButAnonymous(goal.args[0], env)) throw unboundNegation(goal, env);
|
|
353
411
|
const mark = env.mark();
|
|
354
412
|
const iterator = this.solve([goal.args[0]], env, frame.depth + 1);
|
|
355
413
|
let absent;
|
|
@@ -364,11 +422,18 @@ export class Solver {
|
|
|
364
422
|
}
|
|
365
423
|
if (goalKey === 'findall/3') {
|
|
366
424
|
const items = [];
|
|
425
|
+
const scoped = [];
|
|
367
426
|
const uses = recording ? new Set() : null;
|
|
368
427
|
const mark = env.mark();
|
|
369
428
|
for (const [answerEnv, nodes] of this.solve([goal.args[1]], env, frame.depth + 1)) {
|
|
370
429
|
this.serial++;
|
|
371
|
-
|
|
430
|
+
const names = new Map();
|
|
431
|
+
items.push(freshTerm(copyResolved(goal.args[0], answerEnv), `collection${this.serial}`, names));
|
|
432
|
+
if (this.program.inspectionEnabled && names.size) {
|
|
433
|
+
for (const name of scopedVariables(nodes, answerEnv).keys()) {
|
|
434
|
+
if (names.has(name)) scoped.push(names.get(name));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
372
437
|
if (recording) clausesUsed(nodes, uses);
|
|
373
438
|
}
|
|
374
439
|
env.undo(mark);
|
|
@@ -377,10 +442,12 @@ export class Solver {
|
|
|
377
442
|
return null;
|
|
378
443
|
}
|
|
379
444
|
if (!recording) return this.advance(frame, null);
|
|
380
|
-
return this.advance(frame, new Node(goal, COLLECTED, NO_NODES, NO_NODES, uses));
|
|
445
|
+
return this.advance(frame, new Node(goal, COLLECTED, NO_NODES, NO_NODES, uses, false, scoped));
|
|
381
446
|
}
|
|
382
447
|
let point;
|
|
383
|
-
if (goalKey
|
|
448
|
+
if (INSPECTION_KEYS.has(goalKey)) {
|
|
449
|
+
point = new Point('inspection', frame, env.mark(), goal, { alternatives: this.program.inspections.get(goalKey) });
|
|
450
|
+
} else if (goalKey === 'or/2') {
|
|
384
451
|
point = new Point('branch', frame, env.mark(), goal, { alternatives: goal.args });
|
|
385
452
|
} else if (goalKey === 'call/1' || goalKey === 'once/1') {
|
|
386
453
|
const cut = goalKey === 'once/1' ? choices.length : null;
|
|
@@ -410,6 +477,7 @@ export class Solver {
|
|
|
410
477
|
}
|
|
411
478
|
|
|
412
479
|
addFact(goalKey, node) {
|
|
480
|
+
if (node.generic) this.genericPredicates.add(goalKey);
|
|
413
481
|
let facts = this.facts.get(goalKey);
|
|
414
482
|
if (facts === undefined) {
|
|
415
483
|
facts = [];
|
|
@@ -437,6 +505,7 @@ export class Solver {
|
|
|
437
505
|
// The indexes of the derived facts a goal could match, in order, when an
|
|
438
506
|
// argument of the goal narrows them down; null to try them all.
|
|
439
507
|
factOrder(goal, env, goalKey) {
|
|
508
|
+
if (this.genericPredicates.has(goalKey)) return null;
|
|
440
509
|
const positions = this.factIndex.get(goalKey);
|
|
441
510
|
if (positions === undefined) return null;
|
|
442
511
|
let best = null;
|
|
@@ -477,11 +546,21 @@ export class Solver {
|
|
|
477
546
|
return this.childFrame(point.frame, [point.alternatives[position]],
|
|
478
547
|
this.controlPending(point.goal, null), point.frame.depth + 1);
|
|
479
548
|
}
|
|
549
|
+
if (point.kind === 'inspection') {
|
|
550
|
+
const source = point.alternatives[position];
|
|
551
|
+
this.serial++;
|
|
552
|
+
if (unify(point.goal, freshTerm(source.term, this.serial, new Map(), true), env)) {
|
|
553
|
+
return this.advance(point.frame, recording ? new Node(point.goal, new Struct('source', [BigInt(source.id)])) : null);
|
|
554
|
+
}
|
|
555
|
+
env.undo(point.mark);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
480
558
|
const inWindow = point.stop - point.start;
|
|
481
559
|
if (position < inWindow) {
|
|
482
560
|
let index = point.start + position;
|
|
483
561
|
if (point.order !== null) index = point.order[index];
|
|
484
|
-
|
|
562
|
+
let fact = point.facts[index];
|
|
563
|
+
if (fact.generic) fact = freshNode(fact, ++this.serial);
|
|
485
564
|
if (unify(point.goal, fact.goal, env)) {
|
|
486
565
|
if (windowed) this.chosen[point.frame.index] = [0, index];
|
|
487
566
|
return this.advance(point.frame, fact);
|
|
@@ -542,7 +621,7 @@ export class Solver {
|
|
|
542
621
|
break;
|
|
543
622
|
}
|
|
544
623
|
const goalKey = key(goal);
|
|
545
|
-
if (PRIMITIVE_KEYS.has(goalKey) || goalKey === 'not/1' || goalKey === 'findall/3') {
|
|
624
|
+
if (PRIMITIVE_KEYS.has(goalKey) || INSPECTION_KEYS.has(goalKey) || goalKey === 'not/1' || goalKey === 'findall/3') {
|
|
546
625
|
keys.push(null);
|
|
547
626
|
} else if (CONTROL_KEYS.has(goalKey) || (this.program.groups.get(goalKey) || []).some((item) => item.body.length)) {
|
|
548
627
|
keys = null;
|
|
@@ -630,11 +709,27 @@ export class Solver {
|
|
|
630
709
|
// between the heads of one conclusion.
|
|
631
710
|
const resolved = copyResolved(head, answerEnv);
|
|
632
711
|
const unresolved = variables(resolved);
|
|
712
|
+
const captured = clause.captures === null ? null : copyResolved(body[0].args[1], answerEnv);
|
|
713
|
+
const relevant = this.program.inspectionEnabled
|
|
714
|
+
? [...unresolved.keys(), ...(captured === null ? [] : variables(captured).keys())]
|
|
715
|
+
: [];
|
|
716
|
+
// Walking the evidence is linear in its size, so do it only when the
|
|
717
|
+
// conclusion or captures still have variables an inspection could scope.
|
|
718
|
+
if (relevant.length) {
|
|
719
|
+
const scoped = scopedVariables(nodes, answerEnv);
|
|
720
|
+
if (relevant.some((name) => scoped.has(name))) {
|
|
721
|
+
const inBody = new Set();
|
|
722
|
+
for (const goal of clause.body) for (const name of variables(goal).keys()) inBody.add(name);
|
|
723
|
+
if ([...variables(clause.head).keys()].some((name) => !inBody.has(name))) {
|
|
724
|
+
throw new EyemindError('bind inspected rule variables before deriving existential witnesses');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const name of scoped.keys()) unresolved.delete(name);
|
|
728
|
+
}
|
|
633
729
|
if (unresolved.size) {
|
|
634
730
|
// A generated rule also depends on its captures when its head omits
|
|
635
731
|
// them; otherwise two enabled instances could share a witness.
|
|
636
|
-
const
|
|
637
|
-
const witnesses = skolemTerms(clause, head, unresolved, answerEnv, context);
|
|
732
|
+
const witnesses = skolemTerms(clause, head, unresolved, answerEnv, captured);
|
|
638
733
|
[...unresolved.values()].forEach((value, i) => unify(value, witnesses[i], answerEnv));
|
|
639
734
|
}
|
|
640
735
|
const conclusions = unresolved.size
|
|
@@ -678,7 +773,7 @@ export class Solver {
|
|
|
678
773
|
for (const [conclusions, children, bindings, claim] of this.activations(clause)) {
|
|
679
774
|
for (const conclusion of conclusions) {
|
|
680
775
|
if (conclusion === 'true') {
|
|
681
|
-
const claimId = text(claim);
|
|
776
|
+
const claimId = this.program.inspectionEnabled ? variant(claim) : text(claim);
|
|
682
777
|
if (!this.reported.has(claimId)) this.reported.set(claimId, [claim, children, clause.id]);
|
|
683
778
|
continue;
|
|
684
779
|
}
|
|
@@ -688,9 +783,11 @@ export class Solver {
|
|
|
688
783
|
this.haltCode = 65;
|
|
689
784
|
return;
|
|
690
785
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
this.factKeys
|
|
786
|
+
node.generic = this.program.inspectionEnabled && !isGround(conclusion);
|
|
787
|
+
const conclusionId = node.generic ? variant(conclusion) : identity(conclusion);
|
|
788
|
+
const factKeys = node.generic ? this.genericKeys : this.factKeys;
|
|
789
|
+
if (factKeys.has(conclusionId)) continue;
|
|
790
|
+
factKeys.add(conclusionId);
|
|
694
791
|
this.addFact(key(conclusion), node);
|
|
695
792
|
this.derived.push(node);
|
|
696
793
|
this.stats.derived++;
|
package/src/goals.js
CHANGED
|
@@ -25,7 +25,7 @@ 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 (
|
|
28
|
+
if (['implies', 'implied_by', 'forward', 'backward'].includes(name) && args.length === 2) {
|
|
29
29
|
return rebuild(term, args.map(normalizeGoal));
|
|
30
30
|
}
|
|
31
31
|
if ((name === 'not' || name === 'call' || name === 'once') && args.length === 1) {
|
|
@@ -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,8 @@ 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),
|
|
259
291
|
forward: (...args) => {
|
|
260
292
|
if (args.length !== 2) throw new EyemindError(`forward takes 2 arguments, got ${args.length}`);
|
|
261
293
|
return new Struct('forward', args.map(goalOf));
|
|
@@ -686,7 +718,7 @@ function execute(source, filename, builder, io) {
|
|
|
686
718
|
export function loadText(source, filename = '<program>', io = null) {
|
|
687
719
|
const builder = new Builder(() => null);
|
|
688
720
|
execute(String(source).replace(/^#![^\n]*/, ''), filename, builder, io);
|
|
689
|
-
return
|
|
721
|
+
return builder.program();
|
|
690
722
|
}
|
|
691
723
|
|
|
692
724
|
// One Program from several sources, given as [text, filename] pairs, run in
|
|
@@ -696,7 +728,7 @@ export function loadSources(sources, io = null) {
|
|
|
696
728
|
for (const [source, filename] of sources) {
|
|
697
729
|
execute(String(source).replace(/^#![^\n]*/, ''), filename, builder, io);
|
|
698
730
|
}
|
|
699
|
-
return
|
|
731
|
+
return builder.program();
|
|
700
732
|
}
|
|
701
733
|
|
|
702
734
|
// A Program from a function that states clauses when called: a function of a
|
|
@@ -706,7 +738,7 @@ export function build(statements) {
|
|
|
706
738
|
const outer = builders.length ? builders[builders.length - 1] : null;
|
|
707
739
|
const builder = new Builder(outer ? outer.locate : () => null);
|
|
708
740
|
within(builder, () => statements(exportsFor(null)));
|
|
709
|
-
return
|
|
741
|
+
return builder.program();
|
|
710
742
|
}
|
|
711
743
|
|
|
712
744
|
export { FloatLiteral, PREDICATE };
|
package/src/program.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// A program: its clauses, validated, indexed and stratified.
|
|
2
2
|
import { PRIMITIVE_KEYS } from './builtins.js';
|
|
3
|
-
import { ACTIVATION_KEY, expandTemplates } from './templates.js';
|
|
3
|
+
import { ACTIVATION_KEY, containsTemplate, expandTemplates } from './templates.js';
|
|
4
|
+
import { hasInspection, inspectionTerm } from './inspection.js';
|
|
4
5
|
import {
|
|
5
6
|
Env, EyemindError, Struct, Var, deref, flattenConjunction, freshTerm, identity, isCallable,
|
|
6
7
|
isGround, isTerm, key, keyArity, unify,
|
|
7
8
|
} from './terms.js';
|
|
8
9
|
|
|
9
10
|
export const CONTROL_KEYS = new Set(['and/2', 'or/2', 'not/1', 'call/1', 'once/1', 'findall/3']);
|
|
10
|
-
export const RESERVED_KEYS = new Set(['step/4', 'clause/2', ACTIVATION_KEY, 'forward/2', 'backward/2']);
|
|
11
|
+
export const RESERVED_KEYS = new Set(['step/4', 'clause/2', ACTIVATION_KEY, 'implies/2', 'implied_by/2', 'forward/2', 'backward/2']);
|
|
11
12
|
|
|
12
13
|
// The statements that state a forward rule, and how an error names them.
|
|
13
14
|
const FORWARD_KINDS = { forward: 'a forward rule', query: 'query()', contradiction: 'contradiction()' };
|
|
@@ -131,6 +132,8 @@ export class Program {
|
|
|
131
132
|
// clauses: many groups of a large program are never asked for, and a
|
|
132
133
|
// program made to leave one clause out is often run only briefly.
|
|
133
134
|
this.indexes = new Map();
|
|
135
|
+
this.inspectionEnabled = this.clauses.some((clause) => clause.body.some(hasInspection));
|
|
136
|
+
this.inspectionCatalogue = null;
|
|
134
137
|
// Ground source facts are compared against forward conclusions by
|
|
135
138
|
// identity, so work those out once here.
|
|
136
139
|
this.groundFactKeys = new Set();
|
|
@@ -140,6 +143,31 @@ export class Program {
|
|
|
140
143
|
this.strata = stratify(this.clauses, stratifying);
|
|
141
144
|
}
|
|
142
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
|
+
|
|
143
171
|
candidates(goal, env, goalKey) {
|
|
144
172
|
const clauses = this.groups.get(goalKey);
|
|
145
173
|
if (clauses === undefined) return [];
|
package/src/proof.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
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';
|
|
16
17
|
import { isActivation } from './templates.js';
|
|
@@ -320,6 +321,15 @@ function checkSteps(program, steps, allowTrusted, fail, spellings, name) {
|
|
|
320
321
|
}
|
|
321
322
|
if (isTerm(by, 'clause', 1)) {
|
|
322
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++;
|
|
323
333
|
} else if (by === BUILTIN_BY) {
|
|
324
334
|
if (bindings.length || uses.length || !isCallable(goal) || !PRIMITIVE_KEYS.has(key(goal))) {
|
|
325
335
|
fail.add('C3', 'invalid builtin justification', goal);
|
|
@@ -493,6 +503,9 @@ function witnessNames(clause) {
|
|
|
493
503
|
function checkWitnesses(clause, head, names, env) {
|
|
494
504
|
const witnessed = witnessNames(clause);
|
|
495
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;
|
|
496
509
|
let firing = null;
|
|
497
510
|
for (const name of witnessed) {
|
|
498
511
|
const value = copyResolved(names.get(name), env);
|
|
@@ -564,6 +577,9 @@ function checkBoundaries(program, steps, boundaries, fail) {
|
|
|
564
577
|
return [];
|
|
565
578
|
}
|
|
566
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
|
+
}
|
|
567
583
|
const facts = (program.groups.get(goalKey) || [])
|
|
568
584
|
.filter((clause) => !clause.body.length && !clause.forward)
|
|
569
585
|
.map((clause) => freshTerm(clause.head, `evidence${nextSerial()}`));
|
package/src/templates.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Inline rule templates become statically visible clauses whose first
|
|
2
2
|
// premise records the generating activation. No rule is installed at run time.
|
|
3
3
|
import { normalizeGoal } from './goals.js';
|
|
4
|
+
import { isInspection } from './inspection.js';
|
|
4
5
|
import {
|
|
5
6
|
EMPTY, EyemindError, Struct, Var, conjunction, flattenConjunction, isCallable, isTerm, listFromItems, variables,
|
|
6
7
|
} from './terms.js';
|
|
7
8
|
|
|
8
9
|
export const ACTIVATION_KEY = 'activated/2';
|
|
9
10
|
export const isActivation = (term) => isTerm(term, 'activated', 2);
|
|
10
|
-
const isTemplate = (term) =>
|
|
11
|
+
const isTemplate = (term) => term instanceof Struct && term.args.length === 2 &&
|
|
12
|
+
['implies', 'implied_by', 'forward', 'backward'].includes(term.name);
|
|
11
13
|
|
|
12
|
-
function containsTemplate(term) {
|
|
14
|
+
export function containsTemplate(term) {
|
|
13
15
|
const pending = [term];
|
|
14
16
|
while (pending.length) {
|
|
15
17
|
const item = pending.pop();
|
|
@@ -19,31 +21,40 @@ function containsTemplate(term) {
|
|
|
19
21
|
return false;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
function checkGoal(goal, line) {
|
|
23
|
-
if (goal instanceof Var) throw new EyemindError(`line ${line}: generated rule bodies require statically named calls`);
|
|
24
|
-
if (!isCallable(goal) || goal === EMPTY || isTerm(goal, '.', 2)) {
|
|
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))) {
|
|
25
27
|
throw new EyemindError(`line ${line}: a generated rule goal must be an atom or a compound, not a number or list`);
|
|
26
28
|
}
|
|
27
29
|
if (!(goal instanceof Struct)) return;
|
|
30
|
+
if (isInspection(goal)) return;
|
|
28
31
|
if (isTemplate(goal)) throw new EyemindError(`line ${line}: rule templates belong in an implies() conclusion`);
|
|
29
32
|
if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) {
|
|
30
|
-
for (const part of goal.args) checkGoal(part, line);
|
|
33
|
+
for (const part of goal.args) checkGoal(part, line, generated);
|
|
31
34
|
} else if (['not', 'call', 'once'].includes(goal.name) && goal.args.length === 1) {
|
|
32
|
-
checkGoal(goal.args[0], line);
|
|
33
|
-
} else if (isTerm(goal, 'findall', 3))
|
|
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
|
+
}
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export function expandTemplates(sources) {
|
|
37
47
|
const expanded = [...sources];
|
|
38
48
|
for (let index = 0; index < expanded.length; index++) {
|
|
39
49
|
const source = expanded[index];
|
|
50
|
+
for (const goal of source.body) checkGoal(goal, source.line, source.captures !== undefined);
|
|
40
51
|
const heads = flattenConjunction(source.head);
|
|
41
52
|
for (const head of heads) {
|
|
42
53
|
if (isActivation(head)) throw new EyemindError(`line ${source.line}: activated/2 is reserved for generated rules`);
|
|
43
54
|
}
|
|
44
55
|
const templates = heads.filter(isTemplate);
|
|
45
56
|
if (!templates.length) {
|
|
46
|
-
if (containsTemplate(source.head)
|
|
57
|
+
if (containsTemplate(source.head)) {
|
|
47
58
|
throw new EyemindError(`line ${source.line}: rule templates must be top-level conjuncts of an implies() conclusion`);
|
|
48
59
|
}
|
|
49
60
|
continue;
|
|
@@ -51,9 +62,6 @@ export function expandTemplates(sources) {
|
|
|
51
62
|
if (source.kind !== 'forward') {
|
|
52
63
|
throw new EyemindError(`line ${source.line}: rule templates belong in an implies() conclusion`);
|
|
53
64
|
}
|
|
54
|
-
if (source.body.some(containsTemplate)) {
|
|
55
|
-
throw new EyemindError(`line ${source.line}: rule templates belong in an implies() conclusion`);
|
|
56
|
-
}
|
|
57
65
|
// Variables occurring outside a template are captured. Other variables
|
|
58
66
|
// belong to that generated rule and are freshened by ordinary resolution.
|
|
59
67
|
const outer = new Map();
|
|
@@ -64,7 +72,7 @@ export function expandTemplates(sources) {
|
|
|
64
72
|
if (containsTemplate(head)) throw new EyemindError(`line ${source.line}: a rule template cannot be nested inside an ordinary term`);
|
|
65
73
|
return head;
|
|
66
74
|
}
|
|
67
|
-
const forward = head.name === 'forward';
|
|
75
|
+
const forward = head.name === 'implies' || head.name === 'forward';
|
|
68
76
|
const premise = normalizeGoal(head.args[forward ? 0 : 1]);
|
|
69
77
|
const conclusion = normalizeGoal(head.args[forward ? 1 : 0]);
|
|
70
78
|
for (const item of flattenConjunction(conclusion)) {
|
|
@@ -73,7 +81,7 @@ export function expandTemplates(sources) {
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
const body = flattenConjunction(premise);
|
|
76
|
-
for (const goal of body) checkGoal(goal, source.line);
|
|
84
|
+
for (const goal of body) checkGoal(goal, source.line, true);
|
|
77
85
|
const captured = [...variables(head).values()].filter((variable) => outer.has(variable.name));
|
|
78
86
|
const activation = new Struct('activated', [BigInt(expanded.length + 1), listFromItems(captured)]);
|
|
79
87
|
const conclusions = forward ? [conclusion] : flattenConjunction(conclusion);
|
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.28';
|
package/src/writer.js
CHANGED
|
@@ -150,6 +150,34 @@ export class Lettering {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Names for write() that spell variables as the program wrote them, for
|
|
154
|
+
// messages about a running goal: the renamed-apart X#12 is X again and each
|
|
155
|
+
// anonymous variable is _. Distinct variables that share a name are numbered,
|
|
156
|
+
// so a message never shows two of them as one.
|
|
157
|
+
export class SourceNaming {
|
|
158
|
+
constructor() {
|
|
159
|
+
this.names = new Map();
|
|
160
|
+
this.taken = new Set();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get(name) {
|
|
164
|
+
let shown = this.names.get(name);
|
|
165
|
+
if (shown === undefined) {
|
|
166
|
+
const hash = name.indexOf('#');
|
|
167
|
+
const base = hash < 0 ? name : name.slice(0, hash);
|
|
168
|
+
if (/^_[0-9]*$/.test(base)) {
|
|
169
|
+
shown = '_';
|
|
170
|
+
} else {
|
|
171
|
+
shown = variableText(base);
|
|
172
|
+
for (let n = 2; this.taken.has(shown); n++) shown = variableText(`${base}_${n}`);
|
|
173
|
+
this.taken.add(shown);
|
|
174
|
+
}
|
|
175
|
+
this.names.set(name, shown);
|
|
176
|
+
}
|
|
177
|
+
return shown;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
153
181
|
// Names for write() that name nothing but notice a variable.
|
|
154
182
|
class Spy {
|
|
155
183
|
constructor() {
|