eyeprolog 1.3.26 → 1.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/conformance-report.md +2 -1
- package/package.json +1 -1
- package/src/iso.js +28 -5
- package/src/write.js +31 -1
- package/test/conformance/README.md +16 -9
- package/test/conformance/STC-DRAFT-STATUS.md +62 -0
- package/test/conformance/WG17-SYNTAX-STATUS.md +7 -5
- package/test/conformance/cases/stc/bagof_answer_order.pl +6 -0
- package/test/conformance/cases/stc/clause_variable_identity.pl +9 -0
- package/test/conformance/cases/stc/float_underflow_input.pl +10 -0
- package/test/conformance/cases/stc/integer_to_float_evaluable.pl +8 -0
- package/test/conformance/cases/stc/mixed_integer_float_comparison.pl +16 -0
- package/test/conformance/errors/stc/float_overflow_negative_literal.pl +3 -0
- package/test/conformance/errors/stc/float_overflow_negative_number_chars.pl +3 -0
- package/test/conformance/errors/stc/float_overflow_positive_literal.pl +3 -0
- package/test/conformance/errors/stc/float_overflow_positive_number_chars.pl +3 -0
- package/test/conformance/expected/stc/bagof_answer_order.pl +1 -0
- package/test/conformance/expected/stc/clause_variable_identity.pl +1 -0
- package/test/conformance/expected/stc/float_underflow_input.pl +1 -0
- package/test/conformance/expected/stc/integer_to_float_evaluable.pl +1 -0
- package/test/conformance/expected/stc/mixed_integer_float_comparison.pl +1 -0
- package/test/conformance/expected-errors/stc/float_overflow_negative_literal.txt +1 -0
- package/test/conformance/expected-errors/stc/float_overflow_negative_number_chars.txt +1 -0
- package/test/conformance/expected-errors/stc/float_overflow_positive_literal.txt +1 -0
- package/test/conformance/expected-errors/stc/float_overflow_positive_number_chars.txt +1 -0
- package/test/conformance/wg17-syntax-cases.json +7 -7
- package/test/run-interop.mjs +0 -0
- package/test/run-regression.mjs +58 -2
- package/test/run-wg17.mjs +230 -43
- package/the-art-of-eyeprolog.md +1 -1
- 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/conformance-report.md
CHANGED
|
@@ -18,9 +18,10 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
18
18
|
| proofs | 0 | 0 | 0 | 21 | 21 |
|
|
19
19
|
| query | 8 | 2 | 0 | 0 | 10 |
|
|
20
20
|
| rules | 13 | 3 | 0 | 0 | 16 |
|
|
21
|
+
| stc | 5 | 4 | 0 | 0 | 9 |
|
|
21
22
|
| strings | 40 | 0 | 0 | 0 | 40 |
|
|
22
23
|
| syntax | 12 | 23 | 0 | 0 | 35 |
|
|
23
24
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
25
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
26
|
| variables | 16 | 7 | 0 | 0 | 23 |
|
|
26
|
-
| **Total** | **
|
|
27
|
+
| **Total** | **489** | **271** | **19** | **21** | **800** |
|
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');
|
|
@@ -2403,7 +2404,8 @@ function evaluateOperation(term, args) {
|
|
|
2403
2404
|
if (name === '/' && y === 0) throw new PrologError('evaluation_error(zero_divisor)');
|
|
2404
2405
|
let value;
|
|
2405
2406
|
if (name === 'max' || name === 'min') {
|
|
2406
|
-
const
|
|
2407
|
+
const cmp = compareArithmeticValues(args[0], args[1]);
|
|
2408
|
+
const chooseLeft = name === 'max' ? cmp >= 0 : cmp <= 0;
|
|
2407
2409
|
return chooseLeft ? args[0] : args[1];
|
|
2408
2410
|
}
|
|
2409
2411
|
if (name === 'atan2') {
|
|
@@ -2435,12 +2437,33 @@ function numericTerm(value) {
|
|
|
2435
2437
|
export function evaluateArithmetic(term, env) {
|
|
2436
2438
|
return evaluate(term, env);
|
|
2437
2439
|
}
|
|
2440
|
+
function compareIntegerToFloat(integerValue, floatValue) {
|
|
2441
|
+
if (!Number.isFinite(floatValue)) throw new PrologError('evaluation_error(float_overflow)');
|
|
2442
|
+
|
|
2443
|
+
// Do not round an unbounded integer through JavaScript Number before a
|
|
2444
|
+
// mixed arithmetic comparison. Every integral IEEE-754 double can be
|
|
2445
|
+
// converted back to the exact integer value it represents; fractional
|
|
2446
|
+
// doubles necessarily have magnitude below 2^53, so their truncation is
|
|
2447
|
+
// also exact. This preserves mathematical ordering across the I/F boundary
|
|
2448
|
+
// (STC #50), e.g. 9007199254740993 > 9007199254740992.0.
|
|
2449
|
+
if (Number.isInteger(floatValue)) {
|
|
2450
|
+
const floatInteger = BigInt(floatValue);
|
|
2451
|
+
return integerValue < floatInteger ? -1 : integerValue > floatInteger ? 1 : 0;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
const truncated = BigInt(Math.trunc(floatValue));
|
|
2455
|
+
if (integerValue < truncated) return -1;
|
|
2456
|
+
if (integerValue > truncated) return 1;
|
|
2457
|
+
return floatValue > 0 ? -1 : 1;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2438
2460
|
export function compareArithmeticValues(left, right) {
|
|
2439
2461
|
const a = left.value;
|
|
2440
2462
|
const b = right.value;
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2463
|
+
if (left.integer && right.integer) return a < b ? -1 : a > b ? 1 : 0;
|
|
2464
|
+
if (left.integer) return compareIntegerToFloat(a, b);
|
|
2465
|
+
if (right.integer) return -compareIntegerToFloat(b, a);
|
|
2466
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2444
2467
|
}
|
|
2445
2468
|
function* isBuiltin({ goal, env }) {
|
|
2446
2469
|
const result = arithmeticValueTerm(evaluateArithmetic(goal.args[1], env));
|
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);
|
|
@@ -11,6 +11,9 @@ and proof output test the behavior of the JavaScript implementation.
|
|
|
11
11
|
corrigenda, Part 2 modules, and Part 3 definite clause grammars to representative
|
|
12
12
|
executable cases. [WG17-SYNTAX-STATUS.md](WG17-SYNTAX-STATUS.md) records the
|
|
13
13
|
complete one-to-one trace for the vendored active upstream WG17 syntax cases.
|
|
14
|
+
[STC-DRAFT-STATUS.md](STC-DRAFT-STATUS.md) separately tracks executable
|
|
15
|
+
implementation questions from the post-N289 working draft; those cases are
|
|
16
|
+
review evidence, not normative ISO claims.
|
|
14
17
|
|
|
15
18
|
“Conformance” here means conformance to EyeProlog's documented ISO compatibility
|
|
16
19
|
profile and implementation extensions. The default registry covers the exact
|
|
@@ -72,7 +75,7 @@ Run the Part 1 + Corrigenda strict-core processor gate:
|
|
|
72
75
|
npm run test:iso-strict
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
Run
|
|
78
|
+
Run the vendored WG17 conformity matrix independently:
|
|
76
79
|
|
|
77
80
|
```sh
|
|
78
81
|
npm run test:wg17
|
|
@@ -87,12 +90,15 @@ npm run test:wg17
|
|
|
87
90
|
```
|
|
88
91
|
|
|
89
92
|
`wg17:upgrade` reconciles the upstream inventory by identifier: unchanged cases
|
|
90
|
-
keep their reviewed exact outcomes
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
may keep their reviewed exact outcomes as an additional regression lock,
|
|
94
|
+
removed cases disappear, and new or semantically changed cases initially have
|
|
95
|
+
no local snapshot. **Every case is always checked independently against the
|
|
96
|
+
upstream Codex expectation**, so a reviewed EyeProlog outcome can never make a
|
|
97
|
+
non-conforming result pass (the failure mode that previously hid WG17 #227).
|
|
98
|
+
The runner follows the upstream `read(G), G` input protocol, including the
|
|
99
|
+
terminating newline, so stream-sensitive cases such as #270 and #271 exercise
|
|
100
|
+
the characters left after `read/1`. Normal `npm test` remains offline and uses
|
|
101
|
+
only the committed upstream snapshot.
|
|
96
102
|
|
|
97
103
|
Summarize conformance coverage by category:
|
|
98
104
|
|
|
@@ -128,8 +134,9 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
128
134
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
129
135
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
130
136
|
|
|
131
|
-
The corpus has 386 cases in `iso/` and
|
|
132
|
-
total.
|
|
137
|
+
The corpus has 386 cases in `iso/` and 800 file-based conformance cases in
|
|
138
|
+
total. Of those, 9 cases in `stc/` are explicitly labelled working-draft
|
|
139
|
+
review evidence rather than normative ISO claims. The separate strict-reader WG17 matrix has 366 executable dispositions.
|
|
133
140
|
The generated `conformance-report.md` is the authoritative source for current
|
|
134
141
|
category totals. Together with regression, documentation-sync, API, example,
|
|
135
142
|
and book-example checks, `npm test` is the release gate.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# WG17 STC draft review status
|
|
2
|
+
|
|
3
|
+
Source: [post-N289 draft for further technical corrigenda](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc)
|
|
4
|
+
|
|
5
|
+
This file tracks implementation-relevant questions from the WG17 **working
|
|
6
|
+
draft**. It is deliberately separate from the ISO/IEC 13211-1 conformance
|
|
7
|
+
claim: an STC item is a proposal or defect report until WG17 adopts normative
|
|
8
|
+
wording.
|
|
9
|
+
|
|
10
|
+
The purpose of this ledger is practical. Where an STC item can be expressed as
|
|
11
|
+
an executable Prolog observation, EyeProlog keeps a standards-facing case under
|
|
12
|
+
`test/conformance/stc/` (or points to an existing strict/WG17 case) so discussion
|
|
13
|
+
of the draft can expose implementation problems before they become release
|
|
14
|
+
regressions.
|
|
15
|
+
|
|
16
|
+
## Reviewed executable items
|
|
17
|
+
|
|
18
|
+
| STC item | Topic | EyeProlog evidence / finding |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| [#37](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#37) | `clause/2` variable identity | `stc/clause_variable_identity` verifies sharing between head and body is preserved. |
|
|
21
|
+
| [#39](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#39) | negative-number syntax | Covered extensively by the upstream-first WG17 syntax matrix; unary-minus read-back discrepancies found by that matrix were fixed in v1.3.27. |
|
|
22
|
+
| [#40](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#40) | inexact float representation | EyeProlog uses the host IEEE-754 binary64 value and requires written floats to read back to that same value. Input underflow is covered by `stc/float_underflow_input`. |
|
|
23
|
+
| [#42](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#42) | integer-to-float conversion for float functions | `stc/integer_to_float_evaluable` verifies an integer expression is accepted by `sin/1` and produces a float. |
|
|
24
|
+
| [#44](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#44) | `char_code/2` error classification | Existing strict conformance cases require `representation_error(character_code)` for an integer outside the character-code set. |
|
|
25
|
+
| [#49](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#49) | `read_term/3` and EOF | Covered by strict reader/conformance tests and the interactive-read regressions. |
|
|
26
|
+
| [#50](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#50) | mixed integer/float arithmetic comparison | **Found a bug during this audit.** EyeProlog rounded unbounded integers through JavaScript `Number`; `9007199254740993 > 9007199254740992.0` incorrectly failed. `stc/mixed_integer_float_comparison` now requires exact cross-type ordering and `max/2`/`min/2` use the same comparison. |
|
|
27
|
+
| [#55](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#55) | integer rounding function | Existing flag conformance verifies `integer_rounding_function = toward_zero`. |
|
|
28
|
+
| [#58](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#58) | `set_prolog_flag/2` instantiation error | Existing strict/error coverage requires an instantiation error when a required flag value is a variable. |
|
|
29
|
+
| [#67](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#67) | `bagof/3` answer-order example | `stc/bagof_answer_order` verifies the proposed clarifying example produces `[2,1]`. |
|
|
30
|
+
| [#73](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#73) | float-reading limits discussed from issue #54 | Positive/negative literal and `number_chars/2` overflow are kept as explicit draft cases; underflow is tested separately. See the note below. |
|
|
31
|
+
|
|
32
|
+
## Float-reading note for #73
|
|
33
|
+
|
|
34
|
+
EyeProlog currently has a finite-double numeric profile. The draft-facing cases
|
|
35
|
+
record the behavior discussed in issue #54:
|
|
36
|
+
|
|
37
|
+
- a positive finite numeric token beyond the representable range raises
|
|
38
|
+
`representation_error(max_float)`;
|
|
39
|
+
- the corresponding negative overflow raises `representation_error(min_float)`;
|
|
40
|
+
- input underflow may round to `0.0`;
|
|
41
|
+
- overflow produced by arithmetic evaluation remains
|
|
42
|
+
`evaluation_error(float_overflow)`.
|
|
43
|
+
|
|
44
|
+
These `max_float` / `min_float` names are treated as a **provisional draft
|
|
45
|
+
extension**, not as a claim about the currently published ISO core standard.
|
|
46
|
+
The earlier WG17 float-update material also uses
|
|
47
|
+
`representation_error(float_overflow)` for unsupported infinity input, while a
|
|
48
|
+
separate reading-floats proposal discusses `max_float` for a finite value above
|
|
49
|
+
the implementation's largest finite float. Those cases should remain distinct
|
|
50
|
+
when WG17 settles the wording.
|
|
51
|
+
|
|
52
|
+
Relevant background:
|
|
53
|
+
|
|
54
|
+
- [WG17 float update](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/core_update_float-2014-07-21)
|
|
55
|
+
- [Issue #54](https://github.com/eyereasoner/eyeprolog/issues/54)
|
|
56
|
+
|
|
57
|
+
## Maintenance rule
|
|
58
|
+
|
|
59
|
+
When the STC draft changes, review executable changes before a release. Draft
|
|
60
|
+
expectations must never silently replace the normative ISO/WG17 expectations;
|
|
61
|
+
where the two differ, keep the draft test clearly labelled `stc/` until the
|
|
62
|
+
standardization status changes.
|
|
@@ -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
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
% Float-reading draft review associated with STC #73 / issue #54. EyeProlog's
|
|
2
|
+
% finite-double profile permits input underflow to round to zero.
|
|
3
|
+
% https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#73
|
|
4
|
+
%% goal: float_underflow_input
|
|
5
|
+
|
|
6
|
+
float_underflow_input :-
|
|
7
|
+
number_chars(Positive, "1.0e-99999"),
|
|
8
|
+
Positive =:= 0.0,
|
|
9
|
+
number_chars(Negative, "-1.0e-99999"),
|
|
10
|
+
Negative =:= 0.0.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
% STC #42: float evaluable functors such as sin/1 accept integer expressions
|
|
2
|
+
% through the specified integer-to-float conversion.
|
|
3
|
+
% https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#42
|
|
4
|
+
%% goal: integer_to_float_evaluable
|
|
5
|
+
|
|
6
|
+
integer_to_float_evaluable :-
|
|
7
|
+
X is sin(1),
|
|
8
|
+
float(X).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
% STC #50: mixed arithmetic comparisons must not become incorrect by rounding
|
|
2
|
+
% an integer through the float representation before comparing it.
|
|
3
|
+
% https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#50
|
|
4
|
+
%% goal: mixed_integer_float_comparison
|
|
5
|
+
|
|
6
|
+
mixed_integer_float_comparison :-
|
|
7
|
+
9007199254740993 > 9007199254740992.0,
|
|
8
|
+
9007199254740992.0 < 9007199254740993,
|
|
9
|
+
9007199254740993 =\= 9007199254740992.0,
|
|
10
|
+
-9007199254740993 < -9007199254740992.0,
|
|
11
|
+
Max is max(9007199254740993, 9007199254740992.0),
|
|
12
|
+
integer(Max),
|
|
13
|
+
Max =:= 9007199254740993,
|
|
14
|
+
Min is min(9007199254740993, 9007199254740992.0),
|
|
15
|
+
float(Min),
|
|
16
|
+
Min =:= 9007199254740992.0.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bagof_answer_order.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clause_variable_identity.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
float_underflow_input.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
integer_to_float_evaluable.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mixed_integer_float_comparison.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(representation_error(min_float))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(representation_error(min_float))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(representation_error(max_float))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(representation_error(max_float))
|
|
@@ -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, '..');
|
|
@@ -849,6 +849,29 @@ c4 ?- call((!;1)).
|
|
|
849
849
|
assertEqual(hostTermError?.formal, 'evaluation_error(float_overflow)', 'host-created non-finite term');
|
|
850
850
|
},
|
|
851
851
|
},
|
|
852
|
+
{
|
|
853
|
+
name: 'mixed integer/float arithmetic comparison preserves exact order (STC #50)',
|
|
854
|
+
run: () => {
|
|
855
|
+
const hugeInteger = `1${'0'.repeat(309)}`;
|
|
856
|
+
const program = `
|
|
857
|
+
check :-
|
|
858
|
+
9007199254740993 > 9007199254740992.0,
|
|
859
|
+
9007199254740992.0 < 9007199254740993,
|
|
860
|
+
9007199254740993 =\\= 9007199254740992.0,
|
|
861
|
+
-9007199254740993 < -9007199254740992.0,
|
|
862
|
+
${hugeInteger} > 1.0e308,
|
|
863
|
+
-${hugeInteger} < -1.0e308,
|
|
864
|
+
Max is max(9007199254740993, 9007199254740992.0),
|
|
865
|
+
integer(Max),
|
|
866
|
+
Max =:= 9007199254740993,
|
|
867
|
+
Min is min(9007199254740993, 9007199254740992.0),
|
|
868
|
+
float(Min),
|
|
869
|
+
Min =:= 9007199254740992.0.
|
|
870
|
+
`;
|
|
871
|
+
const result = runEyeProlog(program, { goal: 'check' });
|
|
872
|
+
assertEqual(result.stdout, 'check.\n', 'mixed integer/float ordering');
|
|
873
|
+
},
|
|
874
|
+
},
|
|
852
875
|
{
|
|
853
876
|
name: 'readers keep a full stop inside a character-code constant (WG17 #367)',
|
|
854
877
|
run: () => {
|
|
@@ -3236,7 +3259,7 @@ function documentationSyncCases() {
|
|
|
3236
3259
|
},
|
|
3237
3260
|
},
|
|
3238
3261
|
{
|
|
3239
|
-
name: 'WG17
|
|
3262
|
+
name: 'WG17 upstream expectations independently validate reviewed outcomes',
|
|
3240
3263
|
run: () => {
|
|
3241
3264
|
assertEqual(matchesUpstreamExpectation('succeeds', { type: 'success', stages: [] }), true, 'succeeds');
|
|
3242
3265
|
assertEqual(matchesUpstreamExpectation('fails', { type: 'failure' }), true, 'fails');
|
|
@@ -3251,6 +3274,39 @@ function documentationSyncCases() {
|
|
|
3251
3274
|
true,
|
|
3252
3275
|
'observable output',
|
|
3253
3276
|
);
|
|
3277
|
+
const repeated = {
|
|
3278
|
+
id: 227, input: 'write_canonical(B+B).',
|
|
3279
|
+
outcome: { type: 'success', stages: [{ output: '+(_A,_A)', variables: "['B' = B]" }] },
|
|
3280
|
+
};
|
|
3281
|
+
assertEqual(
|
|
3282
|
+
matchesUpstreamExpectation('e.g. +(_1,_1)', repeated.outcome, repeated),
|
|
3283
|
+
true,
|
|
3284
|
+
'anonymous spelling accepted',
|
|
3285
|
+
);
|
|
3286
|
+
assertEqual(
|
|
3287
|
+
matchesUpstreamExpectation(
|
|
3288
|
+
'e.g. +(_1,_1)',
|
|
3289
|
+
{ type: 'success', stages: [{ output: '+(B,B)', variables: "['B' = B]" }] },
|
|
3290
|
+
repeated,
|
|
3291
|
+
),
|
|
3292
|
+
false,
|
|
3293
|
+
'named-variable spelling rejected',
|
|
3294
|
+
);
|
|
3295
|
+
},
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
name: 'WG17 stream-sensitive cases #270 and #271 follow the upstream input protocol',
|
|
3299
|
+
run: () => {
|
|
3300
|
+
const fixture = readWg17SyntaxFixture();
|
|
3301
|
+
const byId = new Map(fixture.cases.map((item) => [item.id, item]));
|
|
3302
|
+
for (const [id, expected] of [[270, "C = ' '"], [271, "C = '%'"]]) {
|
|
3303
|
+
const item = byId.get(id);
|
|
3304
|
+
if (item == null) throw new Error(`missing WG17 #${id}`);
|
|
3305
|
+
assertEqual(item.expected, expected, `WG17 #${id} upstream expectation`);
|
|
3306
|
+
const actual = executeWg17Item(item);
|
|
3307
|
+
assertEqual(matchesUpstreamExpectation(item.expected, actual, item), true, `WG17 #${id} result`);
|
|
3308
|
+
assertEqual(JSON.stringify(actual), JSON.stringify(item.outcome), `WG17 #${id} reviewed outcome`);
|
|
3309
|
+
}
|
|
3254
3310
|
},
|
|
3255
3311
|
},
|
|
3256
3312
|
{
|
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)) {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -7414,7 +7414,7 @@ upstream syntax cases. Reviewed cases can pin exact strict-reader outcomes, whil
|
|
|
7414
7414
|
newly upgraded cases execute directly against the upstream Codex expectation.
|
|
7415
7415
|
|
|
7416
7416
|
The complete suite must pass before release. The file-based conformance corpus
|
|
7417
|
-
contains
|
|
7417
|
+
contains 800 cases, including 386 focused ISO
|
|
7418
7418
|
cases derived from the success, failure, mode, and error behavior in
|
|
7419
7419
|
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
7420
7420
|
Separate exact-output suites check 210 normal
|
|
@@ -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
|
-
}
|