eyeprolog 1.3.26 → 1.3.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/iso.js +2 -1
- package/src/write.js +31 -1
- package/test/conformance/README.md +10 -7
- package/test/conformance/WG17-SYNTAX-STATUS.md +7 -5
- package/test/conformance/wg17-syntax-cases.json +7 -7
- package/test/run-interop.mjs +0 -0
- package/test/run-regression.mjs +35 -2
- package/test/run-wg17.mjs +230 -43
- package/tools/report-wg17-syntax-coverage.mjs +7 -5
- package/tools/upgrade-wg17.mjs +1 -1
- package/test/run-wg17-syntax.mjs +0 -106
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1322,6 +1322,7 @@ function* readTermBuiltin({ solver, goal, env }) {
|
|
|
1322
1322
|
function defaultTermWriteOptions(mode) {
|
|
1323
1323
|
if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1324
1324
|
if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1325
|
+
if (mode === 'write_term') return { quoted: false, ignoreOps: false, numbervars: false, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1325
1326
|
return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1326
1327
|
}
|
|
1327
1328
|
|
|
@@ -1361,7 +1362,7 @@ function writeVariableNames(value, env, option) {
|
|
|
1361
1362
|
return names;
|
|
1362
1363
|
}
|
|
1363
1364
|
|
|
1364
|
-
function termWriteOptions(term, env, mode = '
|
|
1365
|
+
function termWriteOptions(term, env, mode = 'write_term') {
|
|
1365
1366
|
const result = defaultTermWriteOptions(mode);
|
|
1366
1367
|
for (const option of optionList(term, env)) {
|
|
1367
1368
|
if (option.type === VAR) throw new PrologError('instantiation_error');
|
package/src/write.js
CHANGED
|
@@ -143,6 +143,28 @@ function chooseOperator(term, table) {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
function startsWithUnsignedNumber(term, env, table, options) {
|
|
147
|
+
const resolved = deref(term, env);
|
|
148
|
+
if (resolved.type === NUMBER) return !resolved.name.startsWith('-');
|
|
149
|
+
if (resolved.type !== COMPOUND || isCons(resolved)) return false;
|
|
150
|
+
if (options.numbervars && resolved.name === '$VAR' && resolved.arity === 1) {
|
|
151
|
+
const index = deref(resolved.args[0], env);
|
|
152
|
+
if (index.type === NUMBER && /^\d+$/.test(index.name) &&
|
|
153
|
+
writeNumberedVariable(Number(index.name)) != null) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const definition = chooseOperator(resolved, table);
|
|
158
|
+
if (definition == null) return false;
|
|
159
|
+
if (definition.specifier === 'xf' || definition.specifier === 'yf') {
|
|
160
|
+
return startsWithUnsignedNumber(resolved.args[0], env, table, options);
|
|
161
|
+
}
|
|
162
|
+
if (definition.specifier === 'xfx' || definition.specifier === 'xfy' || definition.specifier === 'yfx') {
|
|
163
|
+
return startsWithUnsignedNumber(resolved.args[0], env, table, options);
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
function printableReadVariableNames(term, env, explicit) {
|
|
147
169
|
const names = new Map(explicit);
|
|
148
170
|
const used = new Set(names.values());
|
|
@@ -286,7 +308,15 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
286
308
|
let text;
|
|
287
309
|
if (specifier === 'fx' || specifier === 'fy') {
|
|
288
310
|
const argumentPriority = specifier === 'fx' ? priority - 1 : priority;
|
|
289
|
-
|
|
311
|
+
// A separated `- 1` is still read as the negative number -1. When
|
|
312
|
+
// the source term is the unary -/1 compound, parenthesize a positive
|
|
313
|
+
// numeric-leading argument so writeq/write_term remain read-back safe
|
|
314
|
+
// (WG17 #135, #183, #215, #216, and #248).
|
|
315
|
+
if (resolved.name === '-' && startsWithUnsignedNumber(resolved.args[0], env, table, options)) {
|
|
316
|
+
text = `${token} (${format(resolved.args[0], env, options, table, 1200)})`;
|
|
317
|
+
} else {
|
|
318
|
+
text = `${token} ${format(resolved.args[0], env, options, table, argumentPriority)}`;
|
|
319
|
+
}
|
|
290
320
|
} else if (specifier === 'xf' || specifier === 'yf') {
|
|
291
321
|
let argumentPriority = specifier === 'xf' ? priority - 1 : priority;
|
|
292
322
|
const childDefinition = chooseOperator(deref(resolved.args[0], env), table);
|
|
@@ -72,7 +72,7 @@ Run the Part 1 + Corrigenda strict-core processor gate:
|
|
|
72
72
|
npm run test:iso-strict
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Run
|
|
75
|
+
Run the vendored WG17 conformity matrix independently:
|
|
76
76
|
|
|
77
77
|
```sh
|
|
78
78
|
npm run test:wg17
|
|
@@ -87,12 +87,15 @@ npm run test:wg17
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
`wg17:upgrade` reconciles the upstream inventory by identifier: unchanged cases
|
|
90
|
-
keep their reviewed exact outcomes
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
may keep their reviewed exact outcomes as an additional regression lock,
|
|
91
|
+
removed cases disappear, and new or semantically changed cases initially have
|
|
92
|
+
no local snapshot. **Every case is always checked independently against the
|
|
93
|
+
upstream Codex expectation**, so a reviewed EyeProlog outcome can never make a
|
|
94
|
+
non-conforming result pass (the failure mode that previously hid WG17 #227).
|
|
95
|
+
The runner follows the upstream `read(G), G` input protocol, including the
|
|
96
|
+
terminating newline, so stream-sensitive cases such as #270 and #271 exercise
|
|
97
|
+
the characters left after `read/1`. Normal `npm test` remains offline and uses
|
|
98
|
+
only the committed upstream snapshot.
|
|
96
99
|
|
|
97
100
|
Summarize conformance coverage by category:
|
|
98
101
|
|
|
@@ -4,9 +4,10 @@ Source: [Conformity Testing I: Syntax](https://www.complang.tuwien.ac.at/ulrich/
|
|
|
4
4
|
Upstream inventory checked: 2026-08-16
|
|
5
5
|
|
|
6
6
|
This ledger counts an upstream case when its WG17 identifier, query, and
|
|
7
|
-
expected ISO disposition are stored in the offline executable matrix.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
expected ISO disposition are stored in the offline executable matrix. Every
|
|
8
|
+
case is executed against the upstream Codex expectation. Reviewed exact
|
|
9
|
+
EyeProlog outcomes are additional regression locks and can never override the
|
|
10
|
+
upstream assertion.
|
|
10
11
|
|
|
11
12
|
## Current standing
|
|
12
13
|
|
|
@@ -19,8 +20,9 @@ executed directly against the upstream Codex expectation.
|
|
|
19
20
|
|
|
20
21
|
The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
|
|
21
22
|
3 upstream `waits` cases are checked through EyeProlog's interactive input
|
|
22
|
-
hook.
|
|
23
|
-
365 cases retain exact
|
|
23
|
+
hook. All 366 executable cases are independently checked against the
|
|
24
|
+
upstream Codex expectation. 365 cases additionally retain exact reviewed
|
|
25
|
+
outcomes for stronger regression checking; 1 case currently relies on the upstream assertion alone.
|
|
24
26
|
|
|
25
27
|
## Traceable evidence
|
|
26
28
|
|
|
@@ -1308,7 +1308,7 @@
|
|
|
1308
1308
|
"type": "success",
|
|
1309
1309
|
"stages": [
|
|
1310
1310
|
{
|
|
1311
|
-
"output": "- 1",
|
|
1311
|
+
"output": "- (1)",
|
|
1312
1312
|
"variables": "[]"
|
|
1313
1313
|
}
|
|
1314
1314
|
]
|
|
@@ -1340,7 +1340,7 @@
|
|
|
1340
1340
|
"type": "success",
|
|
1341
1341
|
"stages": [
|
|
1342
1342
|
{
|
|
1343
|
-
"output": "- 1 ^ 2",
|
|
1343
|
+
"output": "- (1 ^ 2)",
|
|
1344
1344
|
"variables": "[]"
|
|
1345
1345
|
}
|
|
1346
1346
|
]
|
|
@@ -1532,7 +1532,7 @@
|
|
|
1532
1532
|
"type": "success",
|
|
1533
1533
|
"stages": [
|
|
1534
1534
|
{
|
|
1535
|
-
"output": "- - 1",
|
|
1535
|
+
"output": "- - (1)",
|
|
1536
1536
|
"variables": "[]"
|
|
1537
1537
|
}
|
|
1538
1538
|
]
|
|
@@ -1552,7 +1552,7 @@
|
|
|
1552
1552
|
"variables": "[]"
|
|
1553
1553
|
},
|
|
1554
1554
|
{
|
|
1555
|
-
"output": "- 1 ~ 2 ~ 3",
|
|
1555
|
+
"output": "- (1 ~ 2 ~ 3)",
|
|
1556
1556
|
"variables": "[]"
|
|
1557
1557
|
}
|
|
1558
1558
|
]
|
|
@@ -1572,7 +1572,7 @@
|
|
|
1572
1572
|
"variables": "[]"
|
|
1573
1573
|
},
|
|
1574
1574
|
{
|
|
1575
|
-
"output": "- 1 ~ 2",
|
|
1575
|
+
"output": "- (1 ~ 2)",
|
|
1576
1576
|
"variables": "[]"
|
|
1577
1577
|
}
|
|
1578
1578
|
]
|
|
@@ -3162,7 +3162,7 @@
|
|
|
3162
3162
|
"type": "success",
|
|
3163
3163
|
"stages": [
|
|
3164
3164
|
{
|
|
3165
|
-
"output": "
|
|
3165
|
+
"output": "$VAR(0)",
|
|
3166
3166
|
"variables": "[]"
|
|
3167
3167
|
}
|
|
3168
3168
|
]
|
|
@@ -5092,7 +5092,7 @@
|
|
|
5092
5092
|
"type": "success",
|
|
5093
5093
|
"stages": [
|
|
5094
5094
|
{
|
|
5095
|
-
"output": "- (1 * 2) ^ 3",
|
|
5095
|
+
"output": "- ((1 * 2) ^ 3)",
|
|
5096
5096
|
"variables": "[]"
|
|
5097
5097
|
}
|
|
5098
5098
|
]
|
package/test/run-interop.mjs
CHANGED
|
File without changes
|
package/test/run-regression.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import { proofExamples } from './run-examples.mjs';
|
|
|
54
54
|
import { goalsFromSource } from './goal-metadata.mjs';
|
|
55
55
|
import { renderWg17SyntaxStatus } from '../tools/report-wg17-syntax-coverage.mjs';
|
|
56
56
|
import { parseWg17SyntaxTable } from '../tools/upgrade-wg17.mjs';
|
|
57
|
-
import { matchesUpstreamExpectation } from './run-wg17.mjs';
|
|
57
|
+
import { executeWg17Item, matchesUpstreamExpectation, readWg17SyntaxFixture } from './run-wg17.mjs';
|
|
58
58
|
|
|
59
59
|
const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
60
60
|
const packageRoot = path.resolve(testRoot, '..');
|
|
@@ -3236,7 +3236,7 @@ function documentationSyncCases() {
|
|
|
3236
3236
|
},
|
|
3237
3237
|
},
|
|
3238
3238
|
{
|
|
3239
|
-
name: 'WG17
|
|
3239
|
+
name: 'WG17 upstream expectations independently validate reviewed outcomes',
|
|
3240
3240
|
run: () => {
|
|
3241
3241
|
assertEqual(matchesUpstreamExpectation('succeeds', { type: 'success', stages: [] }), true, 'succeeds');
|
|
3242
3242
|
assertEqual(matchesUpstreamExpectation('fails', { type: 'failure' }), true, 'fails');
|
|
@@ -3251,6 +3251,39 @@ function documentationSyncCases() {
|
|
|
3251
3251
|
true,
|
|
3252
3252
|
'observable output',
|
|
3253
3253
|
);
|
|
3254
|
+
const repeated = {
|
|
3255
|
+
id: 227, input: 'write_canonical(B+B).',
|
|
3256
|
+
outcome: { type: 'success', stages: [{ output: '+(_A,_A)', variables: "['B' = B]" }] },
|
|
3257
|
+
};
|
|
3258
|
+
assertEqual(
|
|
3259
|
+
matchesUpstreamExpectation('e.g. +(_1,_1)', repeated.outcome, repeated),
|
|
3260
|
+
true,
|
|
3261
|
+
'anonymous spelling accepted',
|
|
3262
|
+
);
|
|
3263
|
+
assertEqual(
|
|
3264
|
+
matchesUpstreamExpectation(
|
|
3265
|
+
'e.g. +(_1,_1)',
|
|
3266
|
+
{ type: 'success', stages: [{ output: '+(B,B)', variables: "['B' = B]" }] },
|
|
3267
|
+
repeated,
|
|
3268
|
+
),
|
|
3269
|
+
false,
|
|
3270
|
+
'named-variable spelling rejected',
|
|
3271
|
+
);
|
|
3272
|
+
},
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
name: 'WG17 stream-sensitive cases #270 and #271 follow the upstream input protocol',
|
|
3276
|
+
run: () => {
|
|
3277
|
+
const fixture = readWg17SyntaxFixture();
|
|
3278
|
+
const byId = new Map(fixture.cases.map((item) => [item.id, item]));
|
|
3279
|
+
for (const [id, expected] of [[270, "C = ' '"], [271, "C = '%'"]]) {
|
|
3280
|
+
const item = byId.get(id);
|
|
3281
|
+
if (item == null) throw new Error(`missing WG17 #${id}`);
|
|
3282
|
+
assertEqual(item.expected, expected, `WG17 #${id} upstream expectation`);
|
|
3283
|
+
const actual = executeWg17Item(item);
|
|
3284
|
+
assertEqual(matchesUpstreamExpectation(item.expected, actual, item), true, `WG17 #${id} result`);
|
|
3285
|
+
assertEqual(JSON.stringify(actual), JSON.stringify(item.outcome), `WG17 #${id} reviewed outcome`);
|
|
3286
|
+
}
|
|
3254
3287
|
},
|
|
3255
3288
|
},
|
|
3256
3289
|
{
|
package/test/run-wg17.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Offline execution of the vendored WG17 conformity-testing syntax matrix.
|
|
3
|
+
// Every case is checked against the upstream Codex expectation. Reviewed
|
|
4
|
+
// exact EyeProlog outcomes are an additional regression lock, never a
|
|
5
|
+
// replacement for the upstream assertion.
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import {
|
|
9
10
|
Env, Program, Solver, parseGoalText, run,
|
|
10
11
|
} from '../src/index.js';
|
|
12
|
+
import { parseTermText } from '../src/parser.js';
|
|
13
|
+
import { variantTerms } from '../src/term.js';
|
|
11
14
|
import { TestReporter, isMainModule } from './test-style.mjs';
|
|
12
15
|
|
|
13
16
|
const testRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -32,6 +35,9 @@ function capturedStages(stdout) {
|
|
|
32
35
|
|
|
33
36
|
function executeFinite(item) {
|
|
34
37
|
try {
|
|
38
|
+
// Match the upstream protocol: the Query cell plus its terminating newline
|
|
39
|
+
// is input to read(G), G (and subsequent read/call stages when present).
|
|
40
|
+
// This is essential for stream-sensitive cases such as #270 and #271.
|
|
35
41
|
const result = run('', {
|
|
36
42
|
isoStrict: true,
|
|
37
43
|
goal: runnerStage(1, item.readCount ?? 16),
|
|
@@ -48,7 +54,7 @@ function executeWait(item) {
|
|
|
48
54
|
const program = Program.parse('', { isoStrict: true });
|
|
49
55
|
const solver = new Solver(program, {
|
|
50
56
|
isoStrict: true,
|
|
51
|
-
ioOptions: { input: item.input },
|
|
57
|
+
ioOptions: { input: `${item.input}\n` },
|
|
52
58
|
});
|
|
53
59
|
const stream = solver.io.resolve('user_input');
|
|
54
60
|
let requests = 0;
|
|
@@ -63,32 +69,203 @@ function executeWait(item) {
|
|
|
63
69
|
try {
|
|
64
70
|
[...solver.solve([goal], new Env(), 0)];
|
|
65
71
|
} catch (_) {
|
|
66
|
-
// Returning null
|
|
67
|
-
//
|
|
72
|
+
// Returning null models EOF only after EyeProlog has requested the extra
|
|
73
|
+
// input that the upstream case classifies as "waits".
|
|
68
74
|
}
|
|
69
75
|
return requests === 1 ? { type: 'waits' } : { type: 'did_not_wait', requests };
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
function
|
|
73
|
-
return String(
|
|
78
|
+
function presentationText(value) {
|
|
79
|
+
return String(value ?? '')
|
|
80
|
+
.replace(/&sup[23];/gi, '')
|
|
74
81
|
.replace(/[²³°]/g, '')
|
|
75
82
|
.replace(/\u00a0/g, ' ')
|
|
76
|
-
.replace(
|
|
83
|
+
.replace(/\r\n?/g, '\n')
|
|
77
84
|
.trim();
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
function canonicalUpstreamExpected(expected) {
|
|
88
|
+
return presentationText(expected).replace(/[ \t\n]+/g, ' ').trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function bindingAnswer(text) {
|
|
92
|
+
let value = String(text ?? '').trim();
|
|
93
|
+
if (value === '[]') return '';
|
|
94
|
+
if (value.startsWith('[') && value.endsWith(']')) value = value.slice(1, -1);
|
|
95
|
+
return value.replace(/'([A-Z_][A-Za-z0-9_]*)'\s*=/g, '$1 =');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function observableCandidates(actual) {
|
|
99
|
+
if (actual.type !== 'success') return [];
|
|
100
|
+
const outputs = actual.stages.map(({ output }) => output ?? '').filter(Boolean);
|
|
101
|
+
const bindings = actual.stages.map(({ variables }) => bindingAnswer(variables)).filter(Boolean);
|
|
102
|
+
const candidates = new Set([...outputs, ...bindings]);
|
|
103
|
+
if (outputs.length > 0) {
|
|
104
|
+
candidates.add(outputs.join(''));
|
|
105
|
+
candidates.add(outputs.join(' '));
|
|
106
|
+
}
|
|
107
|
+
if (bindings.length > 0) candidates.add(bindings.join(', '));
|
|
108
|
+
if (outputs.length > 0 && bindings.length > 0) {
|
|
109
|
+
candidates.add([...outputs, ...bindings].join(' '));
|
|
110
|
+
candidates.add(`${outputs.join('')} ${bindings.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
return [...candidates].filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stripLayoutOutsideQuotes(text) {
|
|
116
|
+
const source = presentationText(text);
|
|
117
|
+
let output = '';
|
|
118
|
+
let quote = null;
|
|
119
|
+
for (let index = 0; index < source.length; index++) {
|
|
120
|
+
const ch = source[index];
|
|
121
|
+
if (quote != null) {
|
|
122
|
+
output += ch;
|
|
123
|
+
if (ch === '\\' && index + 1 < source.length) {
|
|
124
|
+
output += source[++index];
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ch === quote) {
|
|
128
|
+
if (source[index + 1] === quote) output += source[++index];
|
|
129
|
+
else quote = null;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
134
|
+
quote = ch;
|
|
135
|
+
output += ch;
|
|
136
|
+
} else if (!/\s/.test(ch)) {
|
|
137
|
+
output += ch;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return output.replace(/\.$/, '').replace(/([eE])\+(?=\d)/g, '$1');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeExampleVariables(text) {
|
|
144
|
+
const names = new Map();
|
|
145
|
+
let next = 0;
|
|
146
|
+
return String(text).replace(/(?<![A-Za-z0-9_])_[A-Za-z0-9]+/g, (name) => {
|
|
147
|
+
if (!names.has(name)) names.set(name, `_V${++next}`);
|
|
148
|
+
return names.get(name);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function unquoteAtomText(text) {
|
|
153
|
+
const source = text.trim();
|
|
154
|
+
if (!(source.startsWith("'") && source.endsWith("'"))) return source;
|
|
155
|
+
let output = '';
|
|
156
|
+
for (let index = 1; index < source.length - 1; index++) {
|
|
157
|
+
const ch = source[index];
|
|
158
|
+
if (ch === "'" && source[index + 1] === "'") {
|
|
159
|
+
output += "'";
|
|
160
|
+
index++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === '\\' && index + 1 < source.length - 1) {
|
|
164
|
+
const escaped = source[++index];
|
|
165
|
+
const symbolic = { a: '\x07', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' };
|
|
166
|
+
output += symbolic[escaped] ?? escaped;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
output += ch;
|
|
170
|
+
}
|
|
171
|
+
return output;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function splitOperatorNames(text) {
|
|
175
|
+
const source = text.trim();
|
|
176
|
+
if (!source.startsWith('[')) return [unquoteAtomText(source)];
|
|
177
|
+
const body = source.slice(1, -1);
|
|
178
|
+
const names = [];
|
|
179
|
+
let quote = false;
|
|
180
|
+
let start = 0;
|
|
181
|
+
for (let index = 0; index <= body.length; index++) {
|
|
182
|
+
const ch = body[index];
|
|
183
|
+
if (quote) {
|
|
184
|
+
if (ch === "'" && body[index + 1] === "'") index++;
|
|
185
|
+
else if (ch === "'") quote = false;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (ch === "'") {
|
|
189
|
+
quote = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (ch === ',' || index === body.length) {
|
|
193
|
+
names.push(unquoteAtomText(body.slice(start, index)));
|
|
194
|
+
start = index + 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return names.filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function operatorDefinitionsFromInput(input) {
|
|
201
|
+
const definitions = [];
|
|
202
|
+
const pattern = /\bop\(\s*(\d+)\s*,\s*(fx|fy|xf|yf|xfx|xfy|yfx)\s*,\s*(\[[^\]]*\]|'(?:''|[^'])*'|[^)\s,]+)\s*\)/g;
|
|
203
|
+
for (const match of String(input ?? '').matchAll(pattern)) {
|
|
204
|
+
for (const name of splitOperatorNames(match[3])) {
|
|
205
|
+
definitions.push([Number(match[1]), match[2], name]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return definitions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function termEquivalent(expected, actual, item) {
|
|
212
|
+
try {
|
|
213
|
+
const operatorDefinitions = operatorDefinitionsFromInput(item?.input);
|
|
214
|
+
const options = { isoStrict: true, operatorDefinitions };
|
|
215
|
+
const left = parseTermText(`${presentationText(expected).replace(/\.$/, '')}.`, options);
|
|
216
|
+
const right = parseTermText(`${presentationText(actual).replace(/\.$/, '')}.`, options);
|
|
217
|
+
return variantTerms(left, new Env(), right, new Env());
|
|
218
|
+
} catch (_) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function abbreviatedErrorMatches(expected, actual) {
|
|
224
|
+
if (actual.type !== 'error') return false;
|
|
225
|
+
const formal = actual.formal ?? '';
|
|
226
|
+
const patterns = [
|
|
227
|
+
[/p\._e\./i, 'permission_error'],
|
|
228
|
+
[/d\._e\./i, 'domain_error'],
|
|
229
|
+
[/ex\._e\./i, 'existence_error'],
|
|
230
|
+
[/rep(?:r)?\._e\.|repr\.\s*err\./i, 'representation_error'],
|
|
231
|
+
];
|
|
232
|
+
return patterns.some(([pattern, prefix]) => pattern.test(expected) && formal.startsWith(prefix));
|
|
83
233
|
}
|
|
84
234
|
|
|
85
|
-
|
|
235
|
+
function textExpectationMatches(expectedText, actual, item, example = false) {
|
|
236
|
+
const candidates = observableCandidates(actual);
|
|
237
|
+
if (candidates.length === 0) return false;
|
|
238
|
+
let expected = presentationText(expectedText);
|
|
239
|
+
if (example) expected = expected.replace(/^e\.g\.\s*/i, '');
|
|
240
|
+
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
let left = expected;
|
|
243
|
+
let right = candidate;
|
|
244
|
+
if (example && /(?<![A-Za-z0-9_])_[A-Za-z0-9]+/.test(left)) {
|
|
245
|
+
// ISO 7.10.5 requires the generated spelling to be an anonymous-variable
|
|
246
|
+
// token. Normalise only the choice of suffix/name, not the leading `_`.
|
|
247
|
+
if (!/(?<![A-Za-z0-9_])_[A-Za-z0-9]+/.test(right)) continue;
|
|
248
|
+
left = normalizeExampleVariables(left);
|
|
249
|
+
right = normalizeExampleVariables(right);
|
|
250
|
+
}
|
|
251
|
+
if (stripLayoutOutsideQuotes(left) === stripLayoutOutsideQuotes(right)) return true;
|
|
252
|
+
if (termEquivalent(left, right, item)) return true;
|
|
253
|
+
if (example) {
|
|
254
|
+
const expectedNumber = Number(left);
|
|
255
|
+
const actualNumber = Number(right);
|
|
256
|
+
if (Number.isFinite(expectedNumber) && Number.isFinite(actualNumber) &&
|
|
257
|
+
Object.is(expectedNumber, actualNumber)) return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function matchesUpstreamExpectation(expectedText, actual, item = {}) {
|
|
86
264
|
const expected = canonicalUpstreamExpected(expectedText);
|
|
87
265
|
|
|
88
266
|
if (/^waits$/i.test(expected)) return actual.type === 'waits';
|
|
89
267
|
if (/^succeeds(?:\b|$)/i.test(expected)) return actual.type === 'success';
|
|
90
268
|
if (/^fails(?:\b|$)/i.test(expected)) return actual.type === 'failure';
|
|
91
|
-
|
|
92
269
|
if (/^syntax\s*err\.?$/i.test(expected)) {
|
|
93
270
|
return actual.type === 'error' && /^syntax_error\(/.test(actual.formal ?? '');
|
|
94
271
|
}
|
|
@@ -96,26 +273,24 @@ export function matchesUpstreamExpectation(expectedText, actual) {
|
|
|
96
273
|
return actual.type === 'error' && /^representation_error\(/.test(actual.formal ?? '');
|
|
97
274
|
}
|
|
98
275
|
if (/^syntax\/repr\.\s*err\.?$/i.test(expected)) {
|
|
99
|
-
return actual.type === 'error' &&
|
|
100
|
-
/^(?:syntax_error|representation_error)\(/.test(actual.formal ?? '');
|
|
276
|
+
return actual.type === 'error' && /^(?:syntax_error|representation_error)\(/.test(actual.formal ?? '');
|
|
101
277
|
}
|
|
102
278
|
if (/^syntax\s*err\.\/waits$/i.test(expected)) {
|
|
103
279
|
return actual.type === 'waits' ||
|
|
104
280
|
(actual.type === 'error' && /^syntax_error\(/.test(actual.formal ?? ''));
|
|
105
281
|
}
|
|
282
|
+
if (abbreviatedErrorMatches(expected, actual)) return true;
|
|
106
283
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
return false;
|
|
284
|
+
if (/\s+or\s+/i.test(expected)) {
|
|
285
|
+
return expected.split(/\s+or\s+/i)
|
|
286
|
+
.some((alternative) => matchesUpstreamExpectation(alternative, actual, item));
|
|
287
|
+
}
|
|
288
|
+
if (/^e\.g\.\s*/i.test(expected)) return textExpectationMatches(expected, actual, item, true);
|
|
289
|
+
return textExpectationMatches(expected, actual, item, false);
|
|
114
290
|
}
|
|
115
291
|
|
|
116
292
|
function usesWaitMatcher(expectedText) {
|
|
117
|
-
|
|
118
|
-
return /^waits$/i.test(expected);
|
|
293
|
+
return /^waits$/i.test(canonicalUpstreamExpected(expectedText));
|
|
119
294
|
}
|
|
120
295
|
|
|
121
296
|
function compactTestText(value, maximum) {
|
|
@@ -135,30 +310,34 @@ export function wg17TestDescription(item) {
|
|
|
135
310
|
return `#${item.id} ${query} -> ${expected}`;
|
|
136
311
|
}
|
|
137
312
|
|
|
313
|
+
export function executeWg17Item(item) {
|
|
314
|
+
return usesWaitMatcher(item.expected) || item.outcome?.type === 'waits'
|
|
315
|
+
? executeWait(item)
|
|
316
|
+
: executeFinite(item);
|
|
317
|
+
}
|
|
318
|
+
|
|
138
319
|
function assertOutcome(item) {
|
|
139
|
-
|
|
140
|
-
const actual = item.outcome.type === 'waits' ? executeWait(item) : executeFinite(item);
|
|
141
|
-
if (JSON.stringify(actual) !== JSON.stringify(item.outcome)) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`WG17 #${item.id} (${item.expected})\n` +
|
|
144
|
-
`expected ${JSON.stringify(item.outcome)}\n` +
|
|
145
|
-
`actual ${JSON.stringify(actual)}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
320
|
+
const actual = executeWg17Item(item);
|
|
150
321
|
|
|
151
|
-
|
|
152
|
-
if (!matchesUpstreamExpectation(item.expected, actual)) {
|
|
322
|
+
if (!matchesUpstreamExpectation(item.expected, actual, item)) {
|
|
153
323
|
throw new Error(
|
|
154
324
|
`WG17 #${item.id} (${item.expected})\n` +
|
|
155
|
-
`upstream expectation did not match\n` +
|
|
325
|
+
`upstream Codex expectation did not match\n` +
|
|
156
326
|
`actual ${JSON.stringify(actual)}`,
|
|
157
327
|
);
|
|
158
328
|
}
|
|
329
|
+
|
|
330
|
+
if (item.outcome != null && JSON.stringify(actual) !== JSON.stringify(item.outcome)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`WG17 #${item.id} (${item.expected})\n` +
|
|
333
|
+
`reviewed regression outcome changed\n` +
|
|
334
|
+
`expected ${JSON.stringify(item.outcome)}\n` +
|
|
335
|
+
`actual ${JSON.stringify(actual)}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
159
338
|
}
|
|
160
339
|
|
|
161
|
-
function readWg17SyntaxFixture() {
|
|
340
|
+
export function readWg17SyntaxFixture() {
|
|
162
341
|
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
163
342
|
if (!Array.isArray(fixture.cases) || fixture.cases.length === 0) {
|
|
164
343
|
throw new Error('WG17 syntax fixture has no cases');
|
|
@@ -168,6 +347,16 @@ function readWg17SyntaxFixture() {
|
|
|
168
347
|
if (!Number.isInteger(item.id) || ids.has(item.id)) {
|
|
169
348
|
throw new Error(`invalid or duplicate WG17 syntax id #${item.id}`);
|
|
170
349
|
}
|
|
350
|
+
if (typeof item.query !== 'string' || typeof item.input !== 'string' || typeof item.expected !== 'string') {
|
|
351
|
+
throw new Error(`incomplete WG17 syntax fixture row #${item.id}`);
|
|
352
|
+
}
|
|
353
|
+
if (item.outcome != null && !matchesUpstreamExpectation(item.expected, item.outcome, item)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`WG17 #${item.id} reviewed outcome contradicts upstream Codex expectation\n` +
|
|
356
|
+
`upstream ${JSON.stringify(item.expected)}\n` +
|
|
357
|
+
`outcome ${JSON.stringify(item.outcome)}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
171
360
|
ids.add(item.id);
|
|
172
361
|
}
|
|
173
362
|
return fixture;
|
|
@@ -183,10 +372,8 @@ function runWg17Syntax(reporter = new TestReporter()) {
|
|
|
183
372
|
reporter.sectionTotal('WG17 syntax');
|
|
184
373
|
}
|
|
185
374
|
|
|
186
|
-
const suites = [runWg17Syntax];
|
|
187
|
-
|
|
188
375
|
export function runWg17(reporter = new TestReporter()) {
|
|
189
|
-
|
|
376
|
+
runWg17Syntax(reporter);
|
|
190
377
|
}
|
|
191
378
|
|
|
192
379
|
if (isMainModule(import.meta.url)) {
|
|
@@ -92,9 +92,10 @@ Source: [Conformity Testing I: Syntax](${manifest.source})
|
|
|
92
92
|
Upstream inventory checked: ${manifest.checkedOn}
|
|
93
93
|
|
|
94
94
|
This ledger counts an upstream case when its WG17 identifier, query, and
|
|
95
|
-
expected ISO disposition are stored in the offline executable matrix.
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
expected ISO disposition are stored in the offline executable matrix. Every
|
|
96
|
+
case is executed against the upstream Codex expectation. Reviewed exact
|
|
97
|
+
EyeProlog outcomes are additional regression locks and can never override the
|
|
98
|
+
upstream assertion.
|
|
98
99
|
|
|
99
100
|
## Current standing
|
|
100
101
|
|
|
@@ -107,8 +108,9 @@ executed directly against the upstream Codex expectation.
|
|
|
107
108
|
|
|
108
109
|
The matrix runs in strict ISO stream-reader mode as part of \`npm test\`. The
|
|
109
110
|
${waits} upstream \`waits\` case${waits === 1 ? '' : 's'} ${waits === 1 ? 'is' : 'are'} checked through EyeProlog's interactive input
|
|
110
|
-
hook.
|
|
111
|
-
${coveredIds.length - direct} case${coveredIds.length - direct === 1 ? '' : 's'} retain exact
|
|
111
|
+
hook. All ${coveredIds.length} executable cases are independently checked against the
|
|
112
|
+
upstream Codex expectation. ${coveredIds.length - direct} case${coveredIds.length - direct === 1 ? '' : 's'} additionally retain exact reviewed
|
|
113
|
+
outcomes for stronger regression checking; ${direct} case${direct === 1 ? '' : 's'} currently ${direct === 1 ? 'relies' : 'rely'} on the upstream assertion alone.
|
|
112
114
|
|
|
113
115
|
## Traceable evidence
|
|
114
116
|
|
package/tools/upgrade-wg17.mjs
CHANGED
|
@@ -364,7 +364,7 @@ function printHelp() {
|
|
|
364
364
|
process.stdout.write(`Usage: npm run wg17:upgrade -- [--check] [--source URL_OR_FILE]\n\n` +
|
|
365
365
|
`Refreshes the vendored WG17 conformity tests from the TU Wien table.\n` +
|
|
366
366
|
`New or changed rows are executable immediately against the upstream\n` +
|
|
367
|
-
`Codex expectation; existing reviewed exact outcomes remain pinned.\n`);
|
|
367
|
+
`Codex expectation; existing reviewed exact outcomes remain pinned only as additional regression checks.\n`);
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
export async function upgradeWg17({ check = false, source = syntaxSource } = {}) {
|
package/test/run-wg17-syntax.mjs
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Offline, one-by-one execution of the 366 active WG17 syntax cases. The
|
|
3
|
-
// fixture is a dated snapshot of the public conformity-testing table, so the
|
|
4
|
-
// release gate does not depend on the network or another Prolog system.
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import {
|
|
9
|
-
Env, Program, Solver, parseGoalText, run,
|
|
10
|
-
} from '../src/index.js';
|
|
11
|
-
import { TestReporter, isMainModule } from './test-style.mjs';
|
|
12
|
-
|
|
13
|
-
const testRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const fixturePath = path.join(testRoot, 'conformance', 'wg17-syntax-cases.json');
|
|
15
|
-
|
|
16
|
-
function runnerStage(index, maximum) {
|
|
17
|
-
if (index > maximum) return `write('\\n<WG17-COMPLETE>')`;
|
|
18
|
-
return `read_term(G${index}, [variable_names(V${index})]), ` +
|
|
19
|
-
`(G${index} == end_of_file -> write('\\n<WG17-COMPLETE>') ; (` +
|
|
20
|
-
`write('\\n<WG17-BEGIN-${index}>'), call(G${index}), ` +
|
|
21
|
-
`write('<WG17-VARS>'), writeq(V${index}), write('<WG17-END>'), ` +
|
|
22
|
-
`${runnerStage(index + 1, maximum)}))`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function capturedStages(stdout) {
|
|
26
|
-
const complete = stdout.indexOf('<WG17-COMPLETE>');
|
|
27
|
-
if (complete < 0) return null;
|
|
28
|
-
const captured = stdout.slice(0, complete);
|
|
29
|
-
return [...captured.matchAll(/<WG17-BEGIN-(\d+)>([\s\S]*?)<WG17-VARS>([\s\S]*?)<WG17-END>/g)]
|
|
30
|
-
.map((match) => ({ output: match[2], variables: match[3] }));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function executeFinite(item) {
|
|
34
|
-
try {
|
|
35
|
-
const result = run('', {
|
|
36
|
-
isoStrict: true,
|
|
37
|
-
goal: runnerStage(1, item.readCount),
|
|
38
|
-
ioOptions: { input: `${item.input}\n` },
|
|
39
|
-
});
|
|
40
|
-
const stages = capturedStages(result.stdout);
|
|
41
|
-
return stages == null ? { type: 'failure' } : { type: 'success', stages };
|
|
42
|
-
} catch (error) {
|
|
43
|
-
return { type: 'error', formal: error?.formal ?? null };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function executeWait(item) {
|
|
48
|
-
const program = Program.parse('', { isoStrict: true });
|
|
49
|
-
const solver = new Solver(program, {
|
|
50
|
-
isoStrict: true,
|
|
51
|
-
ioOptions: { input: item.input },
|
|
52
|
-
});
|
|
53
|
-
const stream = solver.io.resolve('user_input');
|
|
54
|
-
let requests = 0;
|
|
55
|
-
stream.interactiveReadTerm = () => {
|
|
56
|
-
requests++;
|
|
57
|
-
return null;
|
|
58
|
-
};
|
|
59
|
-
const goal = parseGoalText('read_term(G, [])', {
|
|
60
|
-
isoStrict: true,
|
|
61
|
-
operatorDefinitions: [...program.operators.values()],
|
|
62
|
-
});
|
|
63
|
-
try {
|
|
64
|
-
[...solver.solve([goal], new Env(), 0)];
|
|
65
|
-
} catch (_) {
|
|
66
|
-
// Returning null from the hook models EOF after EyeProlog has asked the
|
|
67
|
-
// interactive source for the continuation that the upstream case awaits.
|
|
68
|
-
}
|
|
69
|
-
return requests === 1 ? { type: 'waits' } : { type: 'did_not_wait', requests };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function assertOutcome(item) {
|
|
73
|
-
const actual = item.outcome.type === 'waits' ? executeWait(item) : executeFinite(item);
|
|
74
|
-
if (JSON.stringify(actual) !== JSON.stringify(item.outcome)) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`WG17 #${item.id} (${item.expected})\n` +
|
|
77
|
-
`expected ${JSON.stringify(item.outcome)}\n` +
|
|
78
|
-
`actual ${JSON.stringify(actual)}`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function runWg17Syntax(reporter = new TestReporter()) {
|
|
84
|
-
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
85
|
-
if (fixture.cases.length !== 366) {
|
|
86
|
-
throw new Error(`WG17 fixture has ${fixture.cases.length} cases instead of 366`);
|
|
87
|
-
}
|
|
88
|
-
const ids = new Set(fixture.cases.map(({ id }) => id));
|
|
89
|
-
if (ids.size !== 366 || ids.has(20) || ids.has(273)) {
|
|
90
|
-
throw new Error('WG17 fixture identifiers do not match the active upstream inventory');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
reporter.section('WG17 syntax');
|
|
94
|
-
for (const item of fixture.cases) reporter.test(`#${item.id}`, () => assertOutcome(item));
|
|
95
|
-
reporter.sectionTotal('WG17 syntax');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (isMainModule(import.meta.url)) {
|
|
99
|
-
const reporter = new TestReporter();
|
|
100
|
-
try {
|
|
101
|
-
runWg17Syntax(reporter);
|
|
102
|
-
reporter.totalLine();
|
|
103
|
-
} catch (_) {
|
|
104
|
-
process.exitCode = 1;
|
|
105
|
-
}
|
|
106
|
-
}
|