eyeprolog 1.1.20 → 1.1.22
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/conformance-report.md +2 -2
- package/package.json +1 -1
- package/src/iso.js +10 -1
- package/src/parser.js +13 -0
- package/src/repl.js +9 -0
- package/src/solver.js +14 -0
- package/src/term.js +16 -3
- package/test/conformance/README.md +1 -1
- package/test/conformance/cases/iso/operator_atoms_as_arguments.pl +7 -0
- package/test/conformance/expected/iso/exceptions_and_flags.pl +1 -1
- package/test/conformance/expected/iso/operator_atoms_as_arguments.pl +2 -0
- package/test/run-regression.mjs +44 -0
- package/the-art-of-eyeprolog.md +33 -5
package/conformance-report.md
CHANGED
|
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
11
11
|
| builtins | 11 | 0 | 0 | 0 | 11 |
|
|
12
12
|
| context | 11 | 0 | 0 | 0 | 11 |
|
|
13
13
|
| control | 15 | 0 | 0 | 0 | 15 |
|
|
14
|
-
| iso |
|
|
14
|
+
| iso | 166 | 209 | 0 | 0 | 375 |
|
|
15
15
|
| lists | 52 | 3 | 0 | 0 | 55 |
|
|
16
16
|
| modules | 2 | 0 | 0 | 0 | 2 |
|
|
17
17
|
| negation | 8 | 0 | 19 | 0 | 27 |
|
|
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
23
23
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
24
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
25
|
| variables | 16 | 9 | 0 | 0 | 25 |
|
|
26
|
-
| **Total** | **
|
|
26
|
+
| **Total** | **481** | **260** | **19** | **21** | **781** |
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -48,7 +48,7 @@ export const isoBuiltins = {
|
|
|
48
48
|
registry.add('!', 0, succeed, { deterministic: true });
|
|
49
49
|
|
|
50
50
|
registry.add('=', 2, unification, { deterministic: true });
|
|
51
|
-
registry.add('unify_with_occurs_check', 2,
|
|
51
|
+
registry.add('unify_with_occurs_check', 2, unificationWithOccursCheck, { deterministic: true });
|
|
52
52
|
registry.add('\\=', 2, nonUnification, { deterministic: true });
|
|
53
53
|
registry.add('subsumes_term', 2, subsumesTermBuiltin, { deterministic: true });
|
|
54
54
|
registry.add('==', 2, identity, { deterministic: true });
|
|
@@ -178,6 +178,14 @@ function* unification({ goal, env }) {
|
|
|
178
178
|
const next = env.clone();
|
|
179
179
|
if (unify(goal.args[0], goal.args[1], next)) yield next;
|
|
180
180
|
}
|
|
181
|
+
function* unificationWithOccursCheck({ goal, env }) {
|
|
182
|
+
const next = env.clone();
|
|
183
|
+
// ISO unify_with_occurs_check/2 always performs finite-tree unification.
|
|
184
|
+
// The implementation-specific occurs_check=error mode applies to normal
|
|
185
|
+
// unification, but must not turn this ISO predicate's ordinary failure into
|
|
186
|
+
// an exception.
|
|
187
|
+
if (unify(goal.args[0], goal.args[1], next, { occursCheck: 'fail' })) yield next;
|
|
188
|
+
}
|
|
181
189
|
function* nonUnification({ goal, env }) {
|
|
182
190
|
if (!unify(goal.args[0], goal.args[1], env.clone())) yield env;
|
|
183
191
|
}
|
|
@@ -1776,6 +1784,7 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
1776
1784
|
}
|
|
1777
1785
|
}
|
|
1778
1786
|
function formalErrorTerm(error) {
|
|
1787
|
+
if (error.formalTerm != null) return compound('error', [error.formalTerm, atom('eyeprolog')]);
|
|
1779
1788
|
const parse = (text) => {
|
|
1780
1789
|
const open = text.indexOf('(');
|
|
1781
1790
|
if (open === -1) return atom(text);
|
package/src/parser.js
CHANGED
|
@@ -225,6 +225,11 @@ class Parser {
|
|
|
225
225
|
}
|
|
226
226
|
operatorTokenName(token = this.token) {
|
|
227
227
|
if (token.type === TOK.ATOM) return token.text;
|
|
228
|
+
// `:-` has its own token because it also introduces clauses/directives,
|
|
229
|
+
// but ISO 6.3.3.1 still permits an operator atom as an argument. Treat the
|
|
230
|
+
// token as the ordinary operator name while parsing terms; the surrounding
|
|
231
|
+
// grammar decides whether it is operator notation or atom data.
|
|
232
|
+
if (token.type === TOK.IF) return ':-';
|
|
228
233
|
if (token.type === TOK.STRING && this.parserFlagState.doubleQuotes === 'atom') return token.text;
|
|
229
234
|
return null;
|
|
230
235
|
}
|
|
@@ -533,6 +538,14 @@ class Parser {
|
|
|
533
538
|
return left;
|
|
534
539
|
}
|
|
535
540
|
parsePrefixTerm(minPrecedence = 0, allowBar = true) {
|
|
541
|
+
// `:-` is tokenized specially so the program grammar can recognize clause
|
|
542
|
+
// and directive markers. In term argument position, however, ISO 6.3.3.1
|
|
543
|
+
// permits an operator atom directly as an `arg`; a leading `:-` cannot be
|
|
544
|
+
// prefix operator notation at argument priority, so it denotes the atom.
|
|
545
|
+
if (this.token.type === TOK.IF) {
|
|
546
|
+
this.advance();
|
|
547
|
+
return atom(':-');
|
|
548
|
+
}
|
|
536
549
|
const operatorName = this.operatorTokenName();
|
|
537
550
|
if (operatorName != null && this.prefixOperators.get(operatorName)?.precedence >= minPrecedence) {
|
|
538
551
|
const op = operatorName;
|
package/src/repl.js
CHANGED
|
@@ -413,6 +413,15 @@ function formatError(engine, state, error) {
|
|
|
413
413
|
const env = new engine.Env();
|
|
414
414
|
const variableNames = new Map();
|
|
415
415
|
let generated = 0;
|
|
416
|
+
if (error.formalTerm != null) {
|
|
417
|
+
collectUnboundVariables(engine, error.formalTerm, env, variableNames, () => `_${letterName(generated++)}`);
|
|
418
|
+
const formal = engine.formatTermForWrite(error.formalTerm, env, {
|
|
419
|
+
quoted: true,
|
|
420
|
+
operators: [...state.program.operators.values()],
|
|
421
|
+
variableNames,
|
|
422
|
+
});
|
|
423
|
+
return `error(${formal}).`;
|
|
424
|
+
}
|
|
416
425
|
if (error.culprit != null) {
|
|
417
426
|
collectUnboundVariables(engine, error.culprit, env, variableNames, () => `_${letterName(generated++)}`);
|
|
418
427
|
}
|
package/src/solver.js
CHANGED
|
@@ -23,6 +23,12 @@ export function nextFreshId() {
|
|
|
23
23
|
return ++freshCounter;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function raiseOccursCheckError(left, right, env) {
|
|
27
|
+
const error = new PrologError('occurs_check');
|
|
28
|
+
error.formalTerm = copyResolved(compound('occurs_check', [left, right]), env);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export class Solver {
|
|
27
33
|
constructor(program, options = {}) {
|
|
28
34
|
this.registry = options.registry ?? getEyePrologRegistry();
|
|
@@ -37,6 +43,11 @@ export class Solver {
|
|
|
37
43
|
this.solutionLimit = options.solutionLimit ?? 10000000;
|
|
38
44
|
this.solutionsSeen = 0;
|
|
39
45
|
this.prologFlags = options.prologFlags ?? defaultPrologFlags(this.registry?.eyePrologLibrary ? 'fail' : 'error');
|
|
46
|
+
this.occursCheckHandler = (left, right, env) => {
|
|
47
|
+
if (this.prologFlags.get('occurs_check')?.value?.name === 'error') {
|
|
48
|
+
raiseOccursCheckError(left, right, env);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
40
51
|
this.charConversions = options.charConversions ?? new Map();
|
|
41
52
|
if (!options.prologFlags) {
|
|
42
53
|
if (['chars', 'codes', 'atom'].includes(program.doubleQuotes)) {
|
|
@@ -140,6 +151,7 @@ export class Solver {
|
|
|
140
151
|
|
|
141
152
|
*solve(goals, env = new Env(), depth = 0) {
|
|
142
153
|
if (!Array.isArray(goals)) goals = [goals];
|
|
154
|
+
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
143
155
|
|
|
144
156
|
const savedActive = this.active;
|
|
145
157
|
let registeredStack = null;
|
|
@@ -194,6 +206,7 @@ export class Solver {
|
|
|
194
206
|
|
|
195
207
|
goals = frame.goals;
|
|
196
208
|
env = frame.env;
|
|
209
|
+
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
197
210
|
depth = frame.depth;
|
|
198
211
|
let active = frame.active;
|
|
199
212
|
|
|
@@ -474,6 +487,7 @@ function defaultPrologFlags(unknown = 'error') {
|
|
|
474
487
|
['max_arity', { value: compound('unbounded', []), allowed: ['unbounded'], changeable: false }],
|
|
475
488
|
['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
|
|
476
489
|
['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
|
|
490
|
+
['occurs_check', { value: compound('true', []), allowed: ['true', 'error'], changeable: true }],
|
|
477
491
|
]);
|
|
478
492
|
}
|
|
479
493
|
|
package/src/term.js
CHANGED
|
@@ -47,6 +47,7 @@ export class Env {
|
|
|
47
47
|
};
|
|
48
48
|
this._delays = null;
|
|
49
49
|
this._clpz = null;
|
|
50
|
+
this._occursCheckHandler = null;
|
|
50
51
|
}
|
|
51
52
|
clone() {
|
|
52
53
|
// Most speculative environments are either rejected without a binding or
|
|
@@ -58,8 +59,13 @@ export class Env {
|
|
|
58
59
|
clone._state = this._state;
|
|
59
60
|
clone._delays = this._delays;
|
|
60
61
|
clone._clpz = this._clpz;
|
|
62
|
+
clone._occursCheckHandler = this._occursCheckHandler;
|
|
61
63
|
return clone;
|
|
62
64
|
}
|
|
65
|
+
setOccursCheckHandler(handler) {
|
|
66
|
+
this._occursCheckHandler = typeof handler === 'function' ? handler : null;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
63
69
|
has(name) {
|
|
64
70
|
return this.get(name) !== undefined;
|
|
65
71
|
}
|
|
@@ -207,11 +213,12 @@ function occurs(variableName, term, env) {
|
|
|
207
213
|
return false;
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
export function unify(left, right, env) {
|
|
216
|
+
export function unify(left, right, env, options = {}) {
|
|
211
217
|
// Iterative unification avoids deep JavaScript recursion on long lists or
|
|
212
218
|
// deeply nested compounds. The occurs check gives EyeProlog finite-tree
|
|
213
219
|
// unification: a variable cannot be bound to a term containing itself.
|
|
214
220
|
// Bindings are written into the supplied Env.
|
|
221
|
+
const occursCheckHandler = options.occursCheck === 'fail' ? null : env?._occursCheckHandler;
|
|
215
222
|
const stack = [[left, right]];
|
|
216
223
|
while (stack.length) {
|
|
217
224
|
let [a, b] = stack.pop();
|
|
@@ -226,12 +233,18 @@ export function unify(left, right, env) {
|
|
|
226
233
|
continue;
|
|
227
234
|
}
|
|
228
235
|
if (a.type === VAR) {
|
|
229
|
-
if (occurs(a.name, b, env))
|
|
236
|
+
if (occurs(a.name, b, env)) {
|
|
237
|
+
occursCheckHandler?.(a, b, env);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
230
240
|
env.bind(a.name, b);
|
|
231
241
|
continue;
|
|
232
242
|
}
|
|
233
243
|
if (b.type === VAR) {
|
|
234
|
-
if (occurs(b.name, a, env))
|
|
244
|
+
if (occurs(b.name, a, env)) {
|
|
245
|
+
occursCheckHandler?.(b, a, env);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
235
248
|
env.bind(b.name, a);
|
|
236
249
|
continue;
|
|
237
250
|
}
|
|
@@ -94,7 +94,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
94
94
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
95
95
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
96
96
|
|
|
97
|
-
The corpus has
|
|
97
|
+
The corpus has 375 cases in `iso/` and 781 file-based conformance cases in
|
|
98
98
|
total. The generated `conformance-report.md` is the authoritative source for
|
|
99
99
|
current category totals. Together with regression, documentation-sync, API,
|
|
100
100
|
example, and book-example checks, `npm test` is the release gate.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
answer(7, red, caught(eyeprolog), first, caught(eyeprolog)).
|
|
2
2
|
answer(7, red, caught(eyeprolog), second, caught(eyeprolog)).
|
|
3
|
-
flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_integer, unbounded), pair(min_integer, unbounded), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars)]).
|
|
3
|
+
flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_integer, unbounded), pair(min_integer, unbounded), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
|
package/test/run-regression.mjs
CHANGED
|
@@ -522,6 +522,40 @@ c4 ?- call((!;1)).
|
|
|
522
522
|
assertEqual(result.stderr, '', 'stderr');
|
|
523
523
|
},
|
|
524
524
|
},
|
|
525
|
+
{
|
|
526
|
+
name: 'occurs_check error mode detects STO while ISO occurs-check unification still fails',
|
|
527
|
+
run: () => {
|
|
528
|
+
const filename = path.join(tmp, `occurs-check-${++tmpCounter}.pl`);
|
|
529
|
+
fs.writeFileSync(filename, ':- set_prolog_flag(occurs_check, error).\nsame(X, X).\n');
|
|
530
|
+
const result = runCli([], {
|
|
531
|
+
input:
|
|
532
|
+
'current_prolog_flag(occurs_check, Mode).\n' +
|
|
533
|
+
'set_prolog_flag(occurs_check, error).\n' +
|
|
534
|
+
'X = f(X).\n' +
|
|
535
|
+
'catch((Y = g(Y)), E, true).\n' +
|
|
536
|
+
'unify_with_occurs_check(Z, h(Z)).\n' +
|
|
537
|
+
`[${sourceAtom(filename)}].\n` +
|
|
538
|
+
'same(W, k(W)).\n' +
|
|
539
|
+
'set_prolog_flag(occurs_check, true).\n' +
|
|
540
|
+
'Q = q(Q).\n' +
|
|
541
|
+
'halt.\n',
|
|
542
|
+
});
|
|
543
|
+
assertEqual(result.status, 0, 'exit status');
|
|
544
|
+
assertEqual(result.stdout,
|
|
545
|
+
'?- Mode = true.\n' +
|
|
546
|
+
'?- true.\n' +
|
|
547
|
+
'?- error(occurs_check(_A, f(_A))).\n' +
|
|
548
|
+
'?- E = error(occurs_check(_A, g(_A)), eyeprolog).\n' +
|
|
549
|
+
'?- false.\n' +
|
|
550
|
+
'?- true.\n' +
|
|
551
|
+
'?- error(occurs_check(_A, k(_A))).\n' +
|
|
552
|
+
'?- true.\n' +
|
|
553
|
+
'?- false.\n' +
|
|
554
|
+
'?- ',
|
|
555
|
+
'stdout');
|
|
556
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
557
|
+
},
|
|
558
|
+
},
|
|
525
559
|
{
|
|
526
560
|
name: 'REPL enumerates and stops answers like the Scryer top level',
|
|
527
561
|
run: () => {
|
|
@@ -906,6 +940,16 @@ c4 ?- call((!;1)).
|
|
|
906
940
|
assertEqual(program.findGroup('b', 0).clauses.length, 1, 'b/0 count');
|
|
907
941
|
},
|
|
908
942
|
},
|
|
943
|
+
{
|
|
944
|
+
name: 'ISO operator atoms are valid functional and list arguments',
|
|
945
|
+
run: () => {
|
|
946
|
+
const source = [
|
|
947
|
+
'operator_argument(ok) :- current_op(1200, xfx, :-), [:-,-] = [:-,-].',
|
|
948
|
+
'',
|
|
949
|
+
].join('\n');
|
|
950
|
+
assertEqual(run(source, { goal: 'operator_argument(ok)' }).stdout, 'operator_argument(ok).\n', 'operator argument syntax');
|
|
951
|
+
},
|
|
952
|
+
},
|
|
909
953
|
{
|
|
910
954
|
name: 'term input keeps dotted operators intact and uses program operators',
|
|
911
955
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -780,6 +780,27 @@ fails rather than constructing a cyclic term:
|
|
|
780
780
|
(X = wrapper(X)).
|
|
781
781
|
```
|
|
782
782
|
|
|
783
|
+
ISO classifies unifications whose outcome depends on an occurs check as
|
|
784
|
+
subject-to-occurs-check (STO). EyeProlog's default remains the sound finite-tree
|
|
785
|
+
behavior above. For diagnosis, EyeProlog additionally provides the
|
|
786
|
+
implementation-specific flag `occurs_check`; setting it to `error` turns a
|
|
787
|
+
normal unification that would otherwise fail because of the occurs check into
|
|
788
|
+
an exception:
|
|
789
|
+
|
|
790
|
+
```eyeprolog
|
|
791
|
+
:- set_prolog_flag(occurs_check, error).
|
|
792
|
+
|
|
793
|
+
sto_example :- X = wrapper(X).
|
|
794
|
+
% error(occurs_check(_A, wrapper(_A)))
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
The supported values are `true` (the default finite-tree behavior) and `error`
|
|
798
|
+
(STO detection). EyeProlog deliberately does not provide `occurs_check=false`,
|
|
799
|
+
because its term model does not construct cyclic terms. The ISO predicate
|
|
800
|
+
`unify_with_occurs_check/2` is independent of the diagnostic flag: it continues
|
|
801
|
+
to perform finite-tree unification and fails on `unify_with_occurs_check(X,
|
|
802
|
+
wrapper(X))` even when `occurs_check` is `error`.
|
|
803
|
+
|
|
783
804
|
### Meaning is not the search strategy
|
|
784
805
|
|
|
785
806
|
EyeProlog's evaluator is goal-directed. It resolves selected goals against facts,
|
|
@@ -5099,7 +5120,11 @@ The fact is exactly `reports(sensor_7, temperature)`. Priority determines
|
|
|
5099
5120
|
binding strength, and `fx`, `fy`, `xf`, `yf`, `xfx`, `xfy`, and `yfx`
|
|
5100
5121
|
determine position and associativity. `current_op/3` inspects the table;
|
|
5101
5122
|
`op(0, Specifier, Name)` removes a definition. Because declarations affect
|
|
5102
|
-
parsing of subsequent text, place them before their first use.
|
|
5123
|
+
parsing of subsequent text, place them before their first use. ISO argument
|
|
5124
|
+
syntax also permits an atom that is currently an operator to appear directly
|
|
5125
|
+
as a functional argument or list element, so forms such as
|
|
5126
|
+
`current_op(Priority, Specifier, :-)` and `[:-,-]` are valid without quoting
|
|
5127
|
+
or parenthesizing those operator atoms.
|
|
5103
5128
|
|
|
5104
5129
|
Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
|
|
5105
5130
|
for an explicitly stateful queue and
|
|
@@ -5610,11 +5635,14 @@ silently changing a static program.
|
|
|
5610
5635
|
| `max_arity` | `unbounded` | `unbounded` | no |
|
|
5611
5636
|
| `unknown` | `fail` | `error`, `fail`, `warning` | yes |
|
|
5612
5637
|
| `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
|
|
5638
|
+
| `occurs_check` | `true` | `true`, `error` | yes |
|
|
5613
5639
|
|
|
5614
5640
|
The isolated ISO-only registry defaults `unknown` to `error`; the normal
|
|
5615
|
-
EyeProlog environment defaults it to `fail`.
|
|
5616
|
-
|
|
5617
|
-
|
|
5641
|
+
EyeProlog environment defaults it to `fail`. The `occurs_check` flag is an
|
|
5642
|
+
EyeProlog diagnostic extension rather than an ISO-defined core flag; its
|
|
5643
|
+
`true` default preserves the engine's finite-tree unification, while `error`
|
|
5644
|
+
reports STO attempts. Operator and flag directives are processed per program
|
|
5645
|
+
rather than changing global JavaScript state. The `double_quotes` setting affects subsequent source text, included
|
|
5618
5646
|
files, command-line and API goal text, and terms read by `read_term/*`:
|
|
5619
5647
|
|
|
5620
5648
|
```text
|
|
@@ -6821,7 +6849,7 @@ node test/run-conformance-report.mjs
|
|
|
6821
6849
|
```
|
|
6822
6850
|
|
|
6823
6851
|
The complete suite must pass before release. The file-based conformance corpus
|
|
6824
|
-
contains
|
|
6852
|
+
contains 781 cases, including 375 focused ISO
|
|
6825
6853
|
cases derived from the success, failure, mode, and error behavior in
|
|
6826
6854
|
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
6827
6855
|
Separate exact-output suites check 189 normal
|