eyeprolog 1.5.46 → 1.5.48
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/ACKNOWLEDGEMENTS.md +3 -3
- package/conformance-report.md +14 -20
- package/package.json +1 -1
- package/src/iso.js +4 -6
- package/src/quads.js +54 -3
- package/test/conformance/cases/iso/part3_grammar_body_constructs.pl +10 -11
- package/test/conformance/cases/iso/part3_phrase_examples.pl +1 -2
- package/test/run-conformance-report.mjs +17 -25
- package/test/run-regression.mjs +39 -0
- package/the-art-of-eyeprolog.md +10 -2
package/ACKNOWLEDGEMENTS.md
CHANGED
|
@@ -6,8 +6,8 @@ Scale) research team, as part of the **Koreografeye** project.
|
|
|
6
6
|
|
|
7
7
|
## Standards
|
|
8
8
|
|
|
9
|
-
Conformance work on this repository
|
|
10
|
-
|
|
9
|
+
Conformance work on this repository targets the following standards.
|
|
10
|
+
Licensed standards documents are not redistributed here:
|
|
11
11
|
|
|
12
12
|
- ISO/IEC 13211-1:1995, *Information technology — Programming languages —
|
|
13
13
|
Prolog — Part 1: General core*
|
|
@@ -15,7 +15,7 @@ which are consulted under a single-user licence and are not redistributed here:
|
|
|
15
15
|
- ISO/IEC 13211-1:1995/Cor.2:2012
|
|
16
16
|
- ISO/IEC 13211-1:1995/Cor.3:2017
|
|
17
17
|
- ISO/IEC 13211-2:2000, *Part 2: Modules*, and its 2013 amendment
|
|
18
|
-
- ISO/IEC TS 13211-3:
|
|
18
|
+
- ISO/IEC TS 13211-3:2025, *Part 3: Definite clause grammar rules*
|
|
19
19
|
|
|
20
20
|
## External conformance corpora
|
|
21
21
|
|
package/conformance-report.md
CHANGED
|
@@ -47,27 +47,21 @@ spelling therefore changes this report even when no corpus file is added or remo
|
|
|
47
47
|
| variables | 16 | 7 | 0 | 0 | 23 |
|
|
48
48
|
| **Total** | **493** | **272** | **19** | **21** | **805** |
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## DCG conformance clarification
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
EyeProlog checks the input and remainder of `phrase/2-3` and reports
|
|
53
|
+
`type_error(list, S)` when an argument is neither a list nor a partial list.
|
|
54
|
+
These implementation-defined checks follow ISO/IEC TS 13211-3:2025,
|
|
55
|
+
8.18.1.3 g and h; this behavior is not a known deviation.
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
The Part 3 implementation target is
|
|
58
|
+
[ISO/IEC TS 13211-3:2025](https://www.iso.org/standard/83635.html).
|
|
59
|
+
The public [error subclause](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/draft-8.18.1.3)
|
|
60
|
+
also specifies `list` for these checks.
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
EyeProlog checks both arguments but reports `type_error(list, S)`.
|
|
62
|
-
Ulrich Neumerkel's `phrase_quad.pl` corpus, which the release gate fetches
|
|
63
|
-
live from TU Wien, accepts only `false` or `type_error(list, S)` for quads
|
|
64
|
-
41 and 42, so the published TS and that corpus cannot both be satisfied.
|
|
65
|
-
The corpus predates the TS. Changing the error term is a one-line edit in
|
|
66
|
-
`phraseSolutions` (`src/iso.js`); it additionally requires relaxing quads
|
|
67
|
-
41-42 of the Neumerkel gate.
|
|
68
|
-
|
|
69
|
-
Note that TS 13211-3, 7.14.7.2 gives `phrase({true}, nonlist, S)` as an
|
|
70
|
-
example which succeeds with `S = nonlist`, which is consistent with the TS
|
|
71
|
-
permitting `phrase/3` to omit the check entirely. EyeProlog prefers the
|
|
72
|
-
stricter diagnostic that the TS also explicitly allows.
|
|
62
|
+
The [phrase comparison](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/phrase)
|
|
63
|
+
links the machine-readable `phrase_quad.pl` corpus. `npm test` fetches and
|
|
64
|
+
runs that corpus live through the Neumerkel gate; the offline regression
|
|
65
|
+
suite runs all 58 vendored quads. Neither expectations nor error matching
|
|
66
|
+
are relaxed for quads 41-44.
|
|
73
67
|
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -3022,12 +3022,10 @@ function* phraseSolutions({ solver, goal, env }, state) {
|
|
|
3022
3022
|
const input = goal.args[1];
|
|
3023
3023
|
const requestedOutput = goal.arity === 2 ? emptyTerminalSequence() : goal.args[2];
|
|
3024
3024
|
validateDcgEmbeddedGoals(grammarBody, input, requestedOutput);
|
|
3025
|
-
// ISO/IEC TS 13211-3:
|
|
3026
|
-
// type_error(
|
|
3027
|
-
//
|
|
3028
|
-
//
|
|
3029
|
-
// for quads 41-42, so the two cannot currently both be satisfied. See
|
|
3030
|
-
// conformance-report.md, "Known deviations".
|
|
3025
|
+
// ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h permit these checks
|
|
3026
|
+
// and specify type_error(list, S) for invalid terminal-sequence arguments.
|
|
3027
|
+
// Keep both diagnostics and the unmodified upstream phrase quad gate.
|
|
3028
|
+
// See conformance-report.md and issue #94.
|
|
3031
3029
|
if (!isListOrPartialList(input, env)) {
|
|
3032
3030
|
throw new PrologError('type_error(list)', deref(input, env));
|
|
3033
3031
|
}
|
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)) {
|
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
% ISO/IEC TS 13211-3:
|
|
2
|
-
% expressed as the phrase/3 identities the TS states for them.
|
|
1
|
+
% ISO/IEC TS 13211-3:2025 grammar-body semantics regressions.
|
|
3
2
|
digit(D) --> [D], { member(D, [a,b]) }.
|
|
4
3
|
|
|
5
|
-
%
|
|
4
|
+
% phrase([], S0, S) is true iff S0 = S.
|
|
6
5
|
answer(empty_sequence, S) :- phrase([], [a,b], S).
|
|
7
6
|
|
|
8
|
-
%
|
|
7
|
+
% phrase((A,B), S0, S) iff phrase(A, S0, S1), phrase(B, S1, S).
|
|
9
8
|
answer(concatenation, S) :- phrase(([a], [b]), [a,b,c], S).
|
|
10
9
|
|
|
11
|
-
%
|
|
10
|
+
% phrase((A;B), S0, S) iff ( phrase(A,S0,S) ; phrase(B,S0,S) ).
|
|
12
11
|
answer(alternative, S) :- phrase(([a] ; [b]), [b,c], S).
|
|
13
12
|
|
|
14
|
-
%
|
|
13
|
+
% if-then-else regression examples.
|
|
15
14
|
answer(ite_13, yes) :- phrase((("1" | "2") -> "3" ; "4"), "13").
|
|
16
15
|
answer(ite_23, yes) :- phrase((("1" | "2") -> "3" ; "4"), "23").
|
|
17
16
|
answer(ite_4, yes) :- phrase((("1" | "2") -> "3" ; "4"), "4").
|
|
18
17
|
answer(ite_single, no) :- \+ phrase((("1" | "2") -> "3" ; "4"), [_]).
|
|
19
18
|
|
|
20
|
-
%
|
|
19
|
+
% phrase({G}, S0, S) is true iff ( G, S0 = S ).
|
|
21
20
|
answer(grammar_body_goal, S) :- phrase({true}, [a], S).
|
|
22
21
|
|
|
23
|
-
%
|
|
22
|
+
% phrase(phrase(NT), S0, S) iff phrase(NT, S0, S).
|
|
24
23
|
answer(phrase_nonterminal, S) :- phrase(phrase([a]), [a,b], S).
|
|
25
24
|
|
|
26
|
-
%
|
|
25
|
+
% phrase(!, S0, S) is true iff S0 = S.
|
|
27
26
|
answer(cut_identity, S) :- phrase(!, [a,b], S).
|
|
28
27
|
|
|
29
|
-
%
|
|
28
|
+
% phrase(\+ GRBody, S0, S) iff ( \+ phrase(GRBody, S0, _), S0 = S ).
|
|
30
29
|
answer(negation_identity, S) :- phrase(\+ [a], [b,c], S).
|
|
31
30
|
answer(negation_blocks, no) :- \+ phrase(\+ [a], [a], _).
|
|
32
31
|
|
|
33
|
-
%
|
|
32
|
+
% ('.')//2 separates a terminal from the rest of the sequence.
|
|
34
33
|
answer(terminal_split, D) :- phrase(digit(D), [b]).
|
|
35
34
|
%% goal: answer(X0, X1)
|
|
@@ -119,7 +119,7 @@ export function formatConformanceReport(report = buildConformanceReport()) {
|
|
|
119
119
|
}
|
|
120
120
|
lines.push(`| **Total** | **${report.total.positive}** | **${report.total.errors}** | **${report.total.warnings}** | **${report.total.proofs}** | **${report.total.total}** |`);
|
|
121
121
|
|
|
122
|
-
lines.push(...
|
|
122
|
+
lines.push(...dcgConformanceSection());
|
|
123
123
|
|
|
124
124
|
if (report.corpusIssues.length > 0) {
|
|
125
125
|
lines.push('', '## Corpus issues', '');
|
|
@@ -133,35 +133,27 @@ export function formatConformanceReport(report = buildConformanceReport()) {
|
|
|
133
133
|
return `${lines.join('\n')}\n`;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
// of living only in a commit message.
|
|
139
|
-
function knownDeviationSection() {
|
|
136
|
+
// Keep the standards clarification in the generated report.
|
|
137
|
+
function dcgConformanceSection() {
|
|
140
138
|
return [
|
|
141
139
|
'',
|
|
142
|
-
'##
|
|
140
|
+
'## DCG conformance clarification',
|
|
143
141
|
'',
|
|
144
|
-
'
|
|
142
|
+
'EyeProlog checks the input and remainder of `phrase/2-3` and reports',
|
|
143
|
+
'`type_error(list, S)` when an argument is neither a list nor a partial list.',
|
|
144
|
+
'These implementation-defined checks follow ISO/IEC TS 13211-3:2025,',
|
|
145
|
+
'8.18.1.3 g and h; this behavior is not a known deviation.',
|
|
145
146
|
'',
|
|
146
|
-
'
|
|
147
|
+
'The Part 3 implementation target is',
|
|
148
|
+
'[ISO/IEC TS 13211-3:2025](https://www.iso.org/standard/83635.html).',
|
|
149
|
+
'The public [error subclause](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/draft-8.18.1.3)',
|
|
150
|
+
'also specifies `list` for these checks.',
|
|
147
151
|
'',
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'',
|
|
153
|
-
'EyeProlog checks both arguments but reports `type_error(list, S)`.',
|
|
154
|
-
'Ulrich Neumerkel\'s `phrase_quad.pl` corpus, which the release gate fetches',
|
|
155
|
-
'live from TU Wien, accepts only `false` or `type_error(list, S)` for quads',
|
|
156
|
-
'41 and 42, so the published TS and that corpus cannot both be satisfied.',
|
|
157
|
-
'The corpus predates the TS. Changing the error term is a one-line edit in',
|
|
158
|
-
'`phraseSolutions` (`src/iso.js`); it additionally requires relaxing quads',
|
|
159
|
-
'41-42 of the Neumerkel gate.',
|
|
160
|
-
'',
|
|
161
|
-
'Note that TS 13211-3, 7.14.7.2 gives `phrase({true}, nonlist, S)` as an',
|
|
162
|
-
'example which succeeds with `S = nonlist`, which is consistent with the TS',
|
|
163
|
-
'permitting `phrase/3` to omit the check entirely. EyeProlog prefers the',
|
|
164
|
-
'stricter diagnostic that the TS also explicitly allows.',
|
|
152
|
+
'The [phrase comparison](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/phrase)',
|
|
153
|
+
'links the machine-readable `phrase_quad.pl` corpus. `npm test` fetches and',
|
|
154
|
+
'runs that corpus live through the Neumerkel gate; the offline regression',
|
|
155
|
+
'suite runs all 58 vendored quads. Neither expectations nor error matching',
|
|
156
|
+
'are relaxed for quads 41-44.',
|
|
165
157
|
'',
|
|
166
158
|
];
|
|
167
159
|
}
|
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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -9663,8 +9663,16 @@ error, including output produced before a later exception. Its argument may be
|
|
|
9663
9663
|
an exact character list/string or a DCG body: terminal sequences,
|
|
9664
9664
|
conjunction/disjunction, `...`/`ad_infinitum` sequence wildcards, and
|
|
9665
9665
|
user-defined DCG nonterminals are matched against the captured characters.
|
|
9666
|
-
|
|
9667
|
-
|
|
9666
|
+
Appending `| other_answer_sequence` accepts any permutation of the preceding
|
|
9667
|
+
complete answer sequence. Substitutions, residual constraints, per-answer output,
|
|
9668
|
+
and duplicate counts must still match; missing or extra answers are failures.
|
|
9669
|
+
A final failure or exception stays at the end. Overlapping wildcard or approximate
|
|
9670
|
+
answer descriptions are matched one-to-one rather than greedily. Search-budget
|
|
9671
|
+
exhaustion remains undecided. Prefix (`...`), negative (`unexpected`), STO, and
|
|
9672
|
+
input/wait annotations are not supported in a permuted sequence.
|
|
9673
|
+
|
|
9674
|
+
For `setof(1, (Y=2 ; Y=1), L)`, the description
|
|
9675
|
+
`Y=2, L=[1] ; Y=1, L=[1] | other_answer_sequence` accepts either group order.
|
|
9668
9676
|
|
|
9669
9677
|
#### STO, loops, and undecided quad results
|
|
9670
9678
|
|