eyeprolog 1.5.47 → 1.5.49
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
CHANGED
|
@@ -53,6 +53,11 @@ EyeProlog checks the input and remainder of `phrase/2-3` and reports
|
|
|
53
53
|
`type_error(list, S)` when an argument is neither a list nor a partial list.
|
|
54
54
|
These implementation-defined checks follow ISO/IEC TS 13211-3:2025,
|
|
55
55
|
8.18.1.3 g and h; this behavior is not a known deviation.
|
|
56
|
+
The checks are optional. EyeProlog elects to perform both consistently.
|
|
57
|
+
Dedicated regressions require the exact error for atomic non-lists and
|
|
58
|
+
improper lists across both arities and both sequence positions, while
|
|
59
|
+
accepting variables, proper lists, and partial lists. The upstream quads
|
|
60
|
+
allow checking and non-checking outcomes and do not prove this policy.
|
|
56
61
|
|
|
57
62
|
The Part 3 implementation target is
|
|
58
63
|
[ISO/IEC TS 13211-3:2025](https://www.iso.org/standard/83635.html).
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -3024,7 +3024,9 @@ function* phraseSolutions({ solver, goal, env }, state) {
|
|
|
3024
3024
|
validateDcgEmbeddedGoals(grammarBody, input, requestedOutput);
|
|
3025
3025
|
// ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h permit these checks
|
|
3026
3026
|
// and specify type_error(list, S) for invalid terminal-sequence arguments.
|
|
3027
|
-
//
|
|
3027
|
+
// EyeProlog elects to perform both optional checks consistently, including
|
|
3028
|
+
// improper lists. The upstream quads allow non-checking outcomes too; the
|
|
3029
|
+
// dedicated regression matrix requires our exact diagnostics.
|
|
3028
3030
|
// See conformance-report.md and issue #94.
|
|
3029
3031
|
if (!isListOrPartialList(input, env)) {
|
|
3030
3032
|
throw new PrologError('type_error(list)', deref(input, env));
|
package/src/quads.js
CHANGED
|
@@ -68,7 +68,18 @@ function checkQuadDescription(program, quad, description, options, context) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function checkDescription(program, quad, description, options, context) {
|
|
71
|
-
const
|
|
71
|
+
const parts = splitOperator(description, '|');
|
|
72
|
+
const alternatives = [];
|
|
73
|
+
const unordered = new Set();
|
|
74
|
+
for (const part of parts) {
|
|
75
|
+
if (part.type === ATOM && part.name === 'other_answer_sequence') {
|
|
76
|
+
const previous = alternatives.at(-1);
|
|
77
|
+
if (previous == null || unordered.has(previous)) {
|
|
78
|
+
return { ok: false, kind: 'malformed', expected: part };
|
|
79
|
+
}
|
|
80
|
+
unordered.add(previous);
|
|
81
|
+
} else alternatives.push(part);
|
|
82
|
+
}
|
|
72
83
|
// Probe an explicitly accepted nontermination outcome before alternatives
|
|
73
84
|
// that would run the same query without a bound.
|
|
74
85
|
const ordered = [...alternatives].sort((left, right) =>
|
|
@@ -80,7 +91,7 @@ function checkDescription(program, quad, description, options, context) {
|
|
|
80
91
|
let unsupported = null;
|
|
81
92
|
let undecided = null;
|
|
82
93
|
for (const alternative of ordered) {
|
|
83
|
-
const checked = checkAlternative(program, quad, alternative, options, context);
|
|
94
|
+
const checked = checkAlternative(program, quad, alternative, options, context, unordered.has(alternative));
|
|
84
95
|
if (checked.ok) return checked;
|
|
85
96
|
if (checked.kind === 'unsupported') unsupported ??= checked;
|
|
86
97
|
if (checked.kind === 'undecided') undecided ??= checked;
|
|
@@ -88,7 +99,7 @@ function checkDescription(program, quad, description, options, context) {
|
|
|
88
99
|
return unsupported ?? undecided ?? { ok: false, kind: 'failed', expected: description };
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
function checkAlternative(program, quad, alternative, options, context) {
|
|
102
|
+
function checkAlternative(program, quad, alternative, options, context, unordered = false) {
|
|
92
103
|
const leaves = splitOperator(alternative, ';').map(describeLeaf);
|
|
93
104
|
const requiresSto = leaves.some((leaf) => leaf.sto);
|
|
94
105
|
const unsupported = leaves.find((leaf) => leaf.unsupported != null)?.unsupported;
|
|
@@ -96,6 +107,13 @@ function checkAlternative(program, quad, alternative, options, context) {
|
|
|
96
107
|
return { ok: false, kind: 'unsupported', expected: unsupported };
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
// A permutation describes a complete sequence, not an arbitrary prefix or
|
|
111
|
+
// a negative assertion. Do not silently weaken those annotations.
|
|
112
|
+
if (unordered && leaves.some((leaf) => leaf.more || leaf.unexpected || leaf.sto ||
|
|
113
|
+
leaf.waits || leaf.input != null || leaf.peek != null)) {
|
|
114
|
+
return { ok: false, kind: 'unsupported', expected: alternative };
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
const ioLeaves = leaves.filter((leaf) => leaf.input != null || leaf.peek != null);
|
|
100
118
|
if (ioLeaves.length > 1 || (ioLeaves.length > 0 && leaves.length !== 1)) {
|
|
101
119
|
return { ok: false, kind: 'malformed', expected: alternative };
|
|
@@ -143,6 +161,8 @@ function checkAlternative(program, quad, alternative, options, context) {
|
|
|
143
161
|
return { ok: leaf.unexpected ? !matches : matches };
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
if (unordered) return checkAnswerPermutation(program, quad.query, leaves, actual, alternative);
|
|
165
|
+
|
|
146
166
|
let position = 0;
|
|
147
167
|
for (const leaf of leaves) {
|
|
148
168
|
if (leaf.more && !leaf.hasExpectation) return { ok: true };
|
|
@@ -186,6 +206,37 @@ function checkAlternative(program, quad, alternative, options, context) {
|
|
|
186
206
|
return { ok: ended };
|
|
187
207
|
}
|
|
188
208
|
|
|
209
|
+
// Match a multiset, preserving duplicate counts. A greedy match is insufficient:
|
|
210
|
+
// wildcard and approximate descriptions may overlap more specific descriptions.
|
|
211
|
+
// Augmenting paths find a one-to-one assignment without enumerating permutations.
|
|
212
|
+
function checkAnswerPermutation(program, query, leaves, actual, alternative) {
|
|
213
|
+
const terminalAt = leaves.findIndex((leaf) => leaf.false || leaf.loops || leaf.error != null);
|
|
214
|
+
if (terminalAt >= 0 && terminalAt !== leaves.length - 1) return { ok: false };
|
|
215
|
+
const answers = terminalAt < 0 ? leaves : leaves.slice(0, -1);
|
|
216
|
+
if (actual.solutions.length > answers.length) return { ok: false };
|
|
217
|
+
if (actual.undecided) return undecidedResult(actual, alternative);
|
|
218
|
+
if (actual.solutions.length !== answers.length) return { ok: false };
|
|
219
|
+
if (terminalAt >= 0) {
|
|
220
|
+
if (!matchLeaf(program, query, leaves[terminalAt], actual, answers.length)) return { ok: false };
|
|
221
|
+
} else if (!actual.complete || actual.error != null || actual.loopObserved) return { ok: false };
|
|
222
|
+
|
|
223
|
+
const edges = answers.map((leaf) => actual.solutions.flatMap((_, position) =>
|
|
224
|
+
matchLeaf(program, query, leaf, actual, position) ? [position] : []));
|
|
225
|
+
const owners = Array(answers.length).fill(-1);
|
|
226
|
+
const assign = (index, seen) => {
|
|
227
|
+
for (const position of edges[index]) {
|
|
228
|
+
if (seen.has(position)) continue;
|
|
229
|
+
seen.add(position);
|
|
230
|
+
if (owners[position] < 0 || assign(owners[position], seen)) {
|
|
231
|
+
owners[position] = index;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
};
|
|
237
|
+
return { ok: answers.every((_, index) => assign(index, new Set())) };
|
|
238
|
+
}
|
|
239
|
+
|
|
189
240
|
function malformedAlternative(query, alternative) {
|
|
190
241
|
const queryNames = new Set(namedVariables(query).map((variable) => variable.name));
|
|
191
242
|
for (const leaf of splitOperator(alternative, ';').map(describeLeaf)) {
|
|
@@ -37,12 +37,23 @@ implementation-dependent negation/if-then choices documented in the reference.
|
|
|
37
37
|
`--iso-strict` leaves `-->/2` as ordinary Part 1 operator syntax and excludes
|
|
38
38
|
Part 3 grammar expansion and `phrase/2-3` from the strict registry.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
EyeProlog elects to perform both implementation-defined terminal-sequence
|
|
41
|
+
checks in ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h. The checks are optional;
|
|
42
|
+
when performed, their specified error type is `list`. EyeProlog consistently
|
|
43
|
+
raises `type_error(list, Culprit)` for a non-list input to `phrase/2` or
|
|
44
|
+
`phrase/3`, and for a non-list remainder to `phrase/3`. The culprit is the
|
|
45
|
+
whole invalid argument, including an improper list, not just its tail.
|
|
46
|
+
Variables, proper lists, and partial lists are accepted by these checks.
|
|
47
|
+
For otherwise valid grammar bodies, validation precedes execution, even when
|
|
48
|
+
the grammar would fail or produce a side effect.
|
|
49
|
+
|
|
50
|
+
The upstream phrase quads permit both checking and non-checking outcomes and
|
|
51
|
+
therefore do not establish this consistency. A dedicated regression matrix in
|
|
52
|
+
`test/run-regression.mjs` requires the exact diagnostic across both arities,
|
|
53
|
+
both sequence positions, atomic and compound non-lists, improper lists at
|
|
54
|
+
several depths, and several grammar bodies. Positive cases protect variables,
|
|
55
|
+
proper lists, and partial lists. These are policy checks, not a relaxation of
|
|
56
|
+
the unmodified upstream corpus.
|
|
46
57
|
|
|
47
58
|
The Part 3 implementation otherwise keeps sequence validation, grammar-body
|
|
48
59
|
callability, variable-body instantiation errors, `phrase/3` steadfastness, and
|
|
@@ -143,6 +143,11 @@ function dcgConformanceSection() {
|
|
|
143
143
|
'`type_error(list, S)` when an argument is neither a list nor a partial list.',
|
|
144
144
|
'These implementation-defined checks follow ISO/IEC TS 13211-3:2025,',
|
|
145
145
|
'8.18.1.3 g and h; this behavior is not a known deviation.',
|
|
146
|
+
'The checks are optional. EyeProlog elects to perform both consistently.',
|
|
147
|
+
'Dedicated regressions require the exact error for atomic non-lists and',
|
|
148
|
+
'improper lists across both arities and both sequence positions, while',
|
|
149
|
+
'accepting variables, proper lists, and partial lists. The upstream quads',
|
|
150
|
+
'allow checking and non-checking outcomes and do not prove this policy.',
|
|
146
151
|
'',
|
|
147
152
|
'The Part 3 implementation target is',
|
|
148
153
|
'[ISO/IEC TS 13211-3:2025](https://www.iso.org/standard/83635.html).',
|
package/test/run-regression.mjs
CHANGED
|
@@ -995,6 +995,45 @@ why(
|
|
|
995
995
|
assertEqual(result.stdout, 'quads: 13 run, 13 passed, 0 failed.\n', 'quad report');
|
|
996
996
|
},
|
|
997
997
|
},
|
|
998
|
+
{
|
|
999
|
+
name: 'runQuads matches other_answer_sequence permutations exactly (issue #95)',
|
|
1000
|
+
run: () => {
|
|
1001
|
+
const cases = [
|
|
1002
|
+
['setof groups', 'setof(1, (Y=2 ; Y=1), L)', 'Y=2,L=[1];Y=1,L=[1]', true],
|
|
1003
|
+
['reverse', '(X=1;X=2;X=3)', 'X=3;X=2;X=1', true],
|
|
1004
|
+
['duplicates', '(X=1;X=1;X=2)', 'X=2;X=1;X=1', true],
|
|
1005
|
+
['wrong multiplicity', '(X=1;X=1;X=2)', 'X=2;X=2;X=1', false],
|
|
1006
|
+
['missing', '(X=1;X=2)', 'X=3;X=2;X=1', false],
|
|
1007
|
+
['extra', '(X=1;X=2;X=3)', 'X=2;X=1', false],
|
|
1008
|
+
['wrong value', '(X=1;X=2)', 'X=3;X=1', false],
|
|
1009
|
+
['overlapping patterns', '(X=f(a);X=f(b))', "X=f('...');X=f(a)", true],
|
|
1010
|
+
['variable sharing', '(X=Y;true)', 'true;X=Y', true],
|
|
1011
|
+
['wrong sharing', '(X=Y;true)', 'X=Y;X=Y', false],
|
|
1012
|
+
['per-answer output', '(X=1,write(a);X=2,write(b))', 'X=2,outputs("b");X=1,outputs("a")', true],
|
|
1013
|
+
['wrong output', '(X=1,write(a);X=2,write(b))', 'X=2,outputs("a");X=1,outputs("b")', false],
|
|
1014
|
+
['unlisted exception', '(X=1;throw(ball))', 'X=1', false],
|
|
1015
|
+
['terminal exception', '(X=1;X=2;throw(ball))', 'X=2;X=1;throw(ball)', true],
|
|
1016
|
+
['terminal failure', '(X=1;X=2)', 'X=2;X=1;false', true],
|
|
1017
|
+
['empty sequence', 'fail', 'false', true],
|
|
1018
|
+
['ordinary order retained', '(X=1;X=2)', 'X=2;X=1', false, false],
|
|
1019
|
+
];
|
|
1020
|
+
for (const [name, query, answers, passes, unordered = true] of cases) {
|
|
1021
|
+
const result = publicApi.runQuads(`?- ${query}.\n ${answers}${unordered ? ' | other_answer_sequence' : ''}.\n`);
|
|
1022
|
+
assertEqual(result.passed, passes ? 1 : 0, name);
|
|
1023
|
+
assertEqual(result.undecided, 0, `${name} decided`);
|
|
1024
|
+
}
|
|
1025
|
+
for (const answers of ['other_answer_sequence', 'other_answer_sequence | X=1',
|
|
1026
|
+
'X=1 | other_answer_sequence | other_answer_sequence']) {
|
|
1027
|
+
const result = publicApi.runQuads(`?- X=1.\n ${answers}.\n`);
|
|
1028
|
+
assertEqual(result.results[0].kind, 'malformed', 'orphan or repeated marker');
|
|
1029
|
+
}
|
|
1030
|
+
const bounded = publicApi.runQuads(
|
|
1031
|
+
'p(0). p(X) :- p(Y), X is Y+1.\n?- p(X).\n X=1;X=0 | other_answer_sequence.\n',
|
|
1032
|
+
{ quadMaxInferences: 1 },
|
|
1033
|
+
);
|
|
1034
|
+
assertEqual(bounded.undecided, 1, 'bounded execution is not proof of a permutation');
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
998
1037
|
{
|
|
999
1038
|
name: 'runQuads supports realistic decimal-precision float descriptions with ~~ (issue #90)',
|
|
1000
1039
|
run: () => {
|
|
@@ -1191,6 +1230,44 @@ why(
|
|
|
1191
1230
|
assertEqual(forbiddenFresh.failed, 1, 'fresh thrown variable is detected');
|
|
1192
1231
|
},
|
|
1193
1232
|
},
|
|
1233
|
+
{
|
|
1234
|
+
name: 'phrase terminal-sequence checks are consistent across arities and list shapes (issue #94)',
|
|
1235
|
+
run: () => {
|
|
1236
|
+
// The upstream quads allow both checking and non-checking processors.
|
|
1237
|
+
// Require EyeProlog's chosen diagnostic, not either portable outcome.
|
|
1238
|
+
const bodies = ['[]', '[a]', '{true}', '{fail}', '{write(reached)}', '!', 'p'];
|
|
1239
|
+
const invalid = ['non_list', '0', 'f(a)', '[a|non_list]', '[a,b|0]', '[a,b,c|f(t)]'];
|
|
1240
|
+
for (const body of bodies) {
|
|
1241
|
+
for (const bad of invalid) {
|
|
1242
|
+
for (const call of [`phrase(${body}, Bad)`, `phrase(${body}, Bad, [])`,
|
|
1243
|
+
`phrase(${body}, [], Bad)`]) {
|
|
1244
|
+
let output = '';
|
|
1245
|
+
runEyeProlog('p --> [a].', {
|
|
1246
|
+
ioOptions: { write: (text) => { output += text; } },
|
|
1247
|
+
goal: `Bad=${bad}, catch((${call}, write(missed)), error(type_error(list, Bad), _), write(ok))`,
|
|
1248
|
+
});
|
|
1249
|
+
assertEqual(output, 'ok', `${call} with ${bad}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
for (const goal of [
|
|
1254
|
+
'phrase([], [])',
|
|
1255
|
+
'phrase([a], [a])',
|
|
1256
|
+
'phrase([a], L), L == [a]',
|
|
1257
|
+
'phrase([a], [a|T]), T == []',
|
|
1258
|
+
'phrase([], S, S), var(S)',
|
|
1259
|
+
'phrase([], [a|T], [a|T]), var(T)',
|
|
1260
|
+
'phrase([], L, [a|T]), L == [a|T], var(T)',
|
|
1261
|
+
'phrase([a], [a|T], T), var(T)',
|
|
1262
|
+
'phrase([f(a),1], [f(a),1])',
|
|
1263
|
+
]) {
|
|
1264
|
+
let output = '';
|
|
1265
|
+
runEyeProlog('', { goal: `(${goal}), write(ok)`,
|
|
1266
|
+
ioOptions: { write: (text) => { output += text; } } });
|
|
1267
|
+
assertEqual(output, 'ok', goal);
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1194
1271
|
{
|
|
1195
1272
|
name: 'runQuads matches the corrected ISO phrase quad boundaries',
|
|
1196
1273
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5870,10 +5870,14 @@ look_ahead(X), [X] --> [X].
|
|
|
5870
5870
|
`phrase(+Body,?Sequence)` accepts or generates a complete sequence.
|
|
5871
5871
|
`phrase(+Body,?Sequence,?Rest)` leaves `Rest` unconsumed and is steadfast in
|
|
5872
5872
|
that argument. A variable body raises `instantiation_error`; a non-callable
|
|
5873
|
-
body raises `type_error(callable)`. EyeProlog
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5873
|
+
body raises `type_error(callable)`. EyeProlog elects to perform the optional
|
|
5874
|
+
terminal-sequence checks of ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h.
|
|
5875
|
+
It consistently reports `type_error(list, Culprit)` for invalid input in both
|
|
5876
|
+
arities and invalid remainder in `phrase/3`, including improper lists.
|
|
5877
|
+
Variables, proper lists, and partial lists pass these checks. Validation
|
|
5878
|
+
precedes grammar execution; an otherwise valid failing grammar does not
|
|
5879
|
+
suppress the diagnostic. Dedicated regressions enforce this policy separately
|
|
5880
|
+
from the portable quads, which accept both checking and non-checking outcomes. See
|
|
5877
5881
|
[`ISO-PART2-PART3-SCOPE.md`](test/conformance/ISO-PART2-PART3-SCOPE.md).
|
|
5878
5882
|
|
|
5879
5883
|
#### A bidirectional expression grammar
|
|
@@ -9663,8 +9667,16 @@ error, including output produced before a later exception. Its argument may be
|
|
|
9663
9667
|
an exact character list/string or a DCG body: terminal sequences,
|
|
9664
9668
|
conjunction/disjunction, `...`/`ad_infinitum` sequence wildcards, and
|
|
9665
9669
|
user-defined DCG nonterminals are matched against the captured characters.
|
|
9666
|
-
|
|
9667
|
-
|
|
9670
|
+
Appending `| other_answer_sequence` accepts any permutation of the preceding
|
|
9671
|
+
complete answer sequence. Substitutions, residual constraints, per-answer output,
|
|
9672
|
+
and duplicate counts must still match; missing or extra answers are failures.
|
|
9673
|
+
A final failure or exception stays at the end. Overlapping wildcard or approximate
|
|
9674
|
+
answer descriptions are matched one-to-one rather than greedily. Search-budget
|
|
9675
|
+
exhaustion remains undecided. Prefix (`...`), negative (`unexpected`), STO, and
|
|
9676
|
+
input/wait annotations are not supported in a permuted sequence.
|
|
9677
|
+
|
|
9678
|
+
For `setof(1, (Y=2 ; Y=1), L)`, the description
|
|
9679
|
+
`Y=2, L=[1] ; Y=1, L=[1] | other_answer_sequence` accepts either group order.
|
|
9668
9680
|
|
|
9669
9681
|
#### STO, loops, and undecided quad results
|
|
9670
9682
|
|