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.
Files changed (33) hide show
  1. package/conformance-report.md +2 -1
  2. package/package.json +1 -1
  3. package/src/iso.js +28 -5
  4. package/src/write.js +31 -1
  5. package/test/conformance/README.md +16 -9
  6. package/test/conformance/STC-DRAFT-STATUS.md +62 -0
  7. package/test/conformance/WG17-SYNTAX-STATUS.md +7 -5
  8. package/test/conformance/cases/stc/bagof_answer_order.pl +6 -0
  9. package/test/conformance/cases/stc/clause_variable_identity.pl +9 -0
  10. package/test/conformance/cases/stc/float_underflow_input.pl +10 -0
  11. package/test/conformance/cases/stc/integer_to_float_evaluable.pl +8 -0
  12. package/test/conformance/cases/stc/mixed_integer_float_comparison.pl +16 -0
  13. package/test/conformance/errors/stc/float_overflow_negative_literal.pl +3 -0
  14. package/test/conformance/errors/stc/float_overflow_negative_number_chars.pl +3 -0
  15. package/test/conformance/errors/stc/float_overflow_positive_literal.pl +3 -0
  16. package/test/conformance/errors/stc/float_overflow_positive_number_chars.pl +3 -0
  17. package/test/conformance/expected/stc/bagof_answer_order.pl +1 -0
  18. package/test/conformance/expected/stc/clause_variable_identity.pl +1 -0
  19. package/test/conformance/expected/stc/float_underflow_input.pl +1 -0
  20. package/test/conformance/expected/stc/integer_to_float_evaluable.pl +1 -0
  21. package/test/conformance/expected/stc/mixed_integer_float_comparison.pl +1 -0
  22. package/test/conformance/expected-errors/stc/float_overflow_negative_literal.txt +1 -0
  23. package/test/conformance/expected-errors/stc/float_overflow_negative_number_chars.txt +1 -0
  24. package/test/conformance/expected-errors/stc/float_overflow_positive_literal.txt +1 -0
  25. package/test/conformance/expected-errors/stc/float_overflow_positive_number_chars.txt +1 -0
  26. package/test/conformance/wg17-syntax-cases.json +7 -7
  27. package/test/run-interop.mjs +0 -0
  28. package/test/run-regression.mjs +58 -2
  29. package/test/run-wg17.mjs +230 -43
  30. package/the-art-of-eyeprolog.md +1 -1
  31. package/tools/report-wg17-syntax-coverage.mjs +7 -5
  32. package/tools/upgrade-wg17.mjs +1 -1
  33. package/test/run-wg17-syntax.mjs +0 -106
@@ -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** | **484** | **267** | **19** | **21** | **791** |
27
+ | **Total** | **489** | **271** | **19** | **21** | **800** |
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.26",
6
+ "version": "1.3.28",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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 = 'write') {
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 chooseLeft = name === 'max' ? x >= y : x <= y;
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
- return left.integer && right.integer
2442
- ? (a < b ? -1 : a > b ? 1 : 0)
2443
- : (Number(a) < Number(b) ? -1 : Number(a) > Number(b) ? 1 : 0);
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
- text = `${token} ${format(resolved.args[0], env, options, table, argumentPriority)}`;
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 all vendored WG17 conformity matrices independently:
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, removed cases disappear, and new or
91
- semantically changed cases are executed directly against the upstream Codex
92
- expectation. This means `npm run test:wg17` can test an upgraded snapshot
93
- immediately without first copying EyeProlog's current behaviour into an
94
- expected result. Normal `npm test` remains offline and uses only the committed
95
- snapshot.
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 791 file-based conformance cases in
132
- total. The separate strict-reader WG17 matrix has 366 executable dispositions.
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. Existing
8
- cases may pin an exact reviewed EyeProlog outcome; newly upgraded cases are
9
- executed directly against the upstream Codex expectation.
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. 1 case uses the upstream Codex expectation directly; the remaining
23
- 365 cases retain exact stored outcomes for stronger regression checking.
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,6 @@
1
+ % STC #67: the clarifying bagof/3 example retains proof/answer order.
2
+ % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#67
3
+ %% goal: bagof_answer_order
4
+
5
+ bagof_answer_order :-
6
+ bagof(X, (X = 2 ; X = 1), [2, 1]).
@@ -0,0 +1,9 @@
1
+ % STC #37: clause/2 must preserve sharing between variables in head and body.
2
+ % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#37
3
+ %% goal: clause_variable_identity
4
+
5
+ a(X) :- b(X).
6
+
7
+ clause_variable_identity :-
8
+ clause(a(A), b(B)),
9
+ A == B.
@@ -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,3 @@
1
+ % Draft float-reading limit review associated with STC #73 / issue #54.
2
+ % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#73
3
+ answer(-1.0e99999).
@@ -0,0 +1,3 @@
1
+ % Draft float-reading limit review associated with STC #73 / issue #54.
2
+ % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#73
3
+ %% goal: number_chars(_, "-1.0e99999")
@@ -0,0 +1,3 @@
1
+ % Draft float-reading limit review associated with STC #73 / issue #54.
2
+ % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#73
3
+ answer(1.0e99999).
@@ -0,0 +1,3 @@
1
+ % Draft float-reading limit review associated with STC #73 / issue #54.
2
+ % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#73
3
+ %% goal: number_chars(_, "1.0e99999")
@@ -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": "A",
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
  ]
File without changes
@@ -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 direct upstream assertions are executable without local outcomes',
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
- // Aggregate all vendored WG17 conformity suites behind one stable entry point.
3
- // The current upstream conformity table is syntax-focused; additional WG17
4
- // suites can be added here without changing npm/CI commands.
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 from the hook models EOF after EyeProlog has asked the
67
- // interactive source for the continuation that the upstream case awaits.
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 canonicalUpstreamExpected(expected) {
73
- return String(expected)
78
+ function presentationText(value) {
79
+ return String(value ?? '')
80
+ .replace(/&sup[23];/gi, '')
74
81
  .replace(/[²³°]/g, '')
75
82
  .replace(/\u00a0/g, ' ')
76
- .replace(/[ \t\n]+/g, ' ')
83
+ .replace(/\r\n?/g, '\n')
77
84
  .trim();
78
85
  }
79
86
 
80
- function observableOutput(actual) {
81
- if (actual.type !== 'success') return null;
82
- return actual.stages.map(({ output }) => output).join('');
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
- export function matchesUpstreamExpectation(expectedText, actual) {
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
- // Most new WG17 rows are observable write/read examples. For those, the
108
- // Codex cell itself is the expected output, so no hand-written local
109
- // outcome is needed before the case can be executed.
110
- const output = observableOutput(actual);
111
- if (output != null && canonicalUpstreamExpected(output) === expected) return true;
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
- const expected = canonicalUpstreamExpected(expectedText);
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
- if (item.outcome != null) {
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
- const actual = usesWaitMatcher(item.expected) ? executeWait(item) : executeFinite(item);
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
- for (const runSuite of suites) runSuite(reporter);
376
+ runWg17Syntax(reporter);
190
377
  }
191
378
 
192
379
  if (isMainModule(import.meta.url)) {
@@ -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 791 cases, including 386 focused ISO
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. Existing
96
- cases may pin an exact reviewed EyeProlog outcome; newly upgraded cases are
97
- executed directly against the upstream Codex expectation.
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. ${direct} case${direct === 1 ? '' : 's'} use${direct === 1 ? 's' : ''} the upstream Codex expectation directly; the remaining
111
- ${coveredIds.length - direct} case${coveredIds.length - direct === 1 ? '' : 's'} retain exact stored outcomes for stronger regression checking.
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
 
@@ -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 } = {}) {
@@ -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
- }