eyeprolog 1.2.30 → 1.2.32
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/README.md +3 -2
- package/package.json +4 -2
- package/test/conformance/ISO-COMPLIANCE.md +3 -3
- package/test/conformance/README.md +19 -3
- package/test/conformance/WG17-SYNTAX-STATUS.md +12 -12
- package/test/conformance/wg17-syntax-cases.json +8 -9
- package/test/conformance/wg17-syntax-coverage.json +7 -4
- package/test/run-all.mjs +2 -2
- package/test/run-regression.mjs +45 -0
- package/test/run-wg17.mjs +201 -0
- package/the-art-of-eyeprolog.md +6 -3
- package/tools/report-wg17-syntax-coverage.mjs +30 -13
- package/tools/upgrade-wg17.mjs +440 -0
- package/why-eyeprolog.md +1 -1
package/README.md
CHANGED
|
@@ -105,8 +105,9 @@ modules, DCGs, quads, libraries, proofs, and the other documented extensions.
|
|
|
105
105
|
The auditable processor-requirement checklist lives in
|
|
106
106
|
[`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md).
|
|
107
107
|
The separate [WG17 syntax ledger](test/conformance/WG17-SYNTAX-STATUS.md)
|
|
108
|
-
records executable dispositions for
|
|
109
|
-
|
|
108
|
+
records executable dispositions for the vendored active upstream WG17 syntax
|
|
109
|
+
cases and runs as part of `npm test`. Reviewed cases can pin exact outcomes;
|
|
110
|
+
newly upgraded cases run directly against the upstream Codex expectation.
|
|
110
111
|
EyeProlog does not yet claim independent certification or closure of every
|
|
111
112
|
normative Part 1 requirement.
|
|
112
113
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.32",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
"test:eyeprolog": "node test/run-all.mjs",
|
|
51
51
|
"test:conformance": "node test/run-conformance.mjs",
|
|
52
52
|
"test:iso-strict": "node test/run-iso-strict.mjs",
|
|
53
|
-
"test:wg17
|
|
53
|
+
"test:wg17": "node test/run-wg17.mjs",
|
|
54
54
|
"test:examples": "node test/run-examples.mjs",
|
|
55
55
|
"test:regression": "node test/run-regression.mjs",
|
|
56
56
|
"test:playground": "node test/run-playground.mjs",
|
|
57
|
+
"wg17:upgrade": "node tools/upgrade-wg17.mjs",
|
|
58
|
+
"report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
|
|
57
59
|
"report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
|
|
58
60
|
"preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
|
|
59
61
|
"postversion": "git push origin HEAD --follow-tags"
|
|
@@ -18,7 +18,7 @@ error-ordering alternative to an individual executable assertion.
|
|
|
18
18
|
|
|
19
19
|
| Requirement | Status | EyeProlog evidence / remaining work |
|
|
20
20
|
| --- | --- | --- |
|
|
21
|
-
| 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete
|
|
21
|
+
| 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete vendored WG17 syntax matrix](WG17-SYNTAX-STATUS.md). Wider shall-by-shall text-processing audit remains open. |
|
|
22
22
|
| 5.1(b) execute conforming Prolog goals | audit | Clause 7-9 conformance corpus plus regression/API/example gates. A normative goal-semantics ledger is still being expanded. |
|
|
23
23
|
| 5.1(c) reject nonconforming text/read-terms | audit | Dedicated syntax-error cases and strict-core extension rejection. Exhaustive lexical rejection coverage remains open. |
|
|
24
24
|
| 5.1(d) document permitted variations | audit | Major implementation-defined choices are documented in *The Art of EyeProlog*. Every occurrence of “implementation defined/dependent/specific” in Part 1 still needs a final documentation cross-check. |
|
|
@@ -30,7 +30,7 @@ error-ordering alternative to an individual executable assertion.
|
|
|
30
30
|
|
|
31
31
|
| Standard area | Status | Current evidence |
|
|
32
32
|
| --- | --- | --- |
|
|
33
|
-
| Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete
|
|
33
|
+
| Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete vendored WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, and writer/read-back regressions. |
|
|
34
34
|
| 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates. |
|
|
35
35
|
| 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Cross-text `multifile/1` and ordering constraints require explicit shall-by-shall audit. |
|
|
36
36
|
| 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
|
|
@@ -75,7 +75,7 @@ A release intended to advance ISO conformance must pass all of:
|
|
|
75
75
|
npm test
|
|
76
76
|
npm run test:iso-strict
|
|
77
77
|
npm run test:conformance
|
|
78
|
-
npm run test:wg17
|
|
78
|
+
npm run test:wg17
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
The unified `npm test` gate includes the strict-core suite. Expected conformance
|
|
@@ -10,7 +10,7 @@ and proof output test the behavior of the JavaScript implementation.
|
|
|
10
10
|
[ISO-MATRIX.md](ISO-MATRIX.md) maps Part 1 normative clause families, all three
|
|
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
|
-
complete one-to-one trace for
|
|
13
|
+
complete one-to-one trace for the vendored active upstream WG17 syntax cases.
|
|
14
14
|
|
|
15
15
|
“Conformance” here means conformance to EyeProlog's documented ISO compatibility
|
|
16
16
|
profile and implementation extensions. The default registry covers the exact
|
|
@@ -69,12 +69,28 @@ Run the Part 1 + Corrigenda strict-core processor gate:
|
|
|
69
69
|
npm run test:iso-strict
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
Run
|
|
72
|
+
Run all vendored WG17 conformity matrices independently:
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
|
-
npm run test:wg17
|
|
75
|
+
npm run test:wg17
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Refresh the WG17 snapshot from the TU Wien conformity tables before a release
|
|
79
|
+
or whenever upstream changes:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npm run wg17:upgrade
|
|
83
|
+
npm run test:wg17
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`wg17:upgrade` reconciles the upstream inventory by identifier: unchanged cases
|
|
87
|
+
keep their reviewed exact outcomes, removed cases disappear, and new or
|
|
88
|
+
semantically changed cases are executed directly against the upstream Codex
|
|
89
|
+
expectation. This means `npm run test:wg17` can test an upgraded snapshot
|
|
90
|
+
immediately without first copying EyeProlog's current behaviour into an
|
|
91
|
+
expected result. Normal `npm test` remains offline and uses only the committed
|
|
92
|
+
snapshot.
|
|
93
|
+
|
|
78
94
|
Summarize conformance coverage by category:
|
|
79
95
|
|
|
80
96
|
```sh
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# WG17 syntax traceability status
|
|
2
2
|
|
|
3
3
|
Source: [Conformity Testing I: Syntax](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing)
|
|
4
|
-
Upstream inventory checked: 2026-08-
|
|
4
|
+
Upstream inventory checked: 2026-08-16
|
|
5
5
|
|
|
6
|
-
This ledger counts an upstream case
|
|
7
|
-
expected ISO disposition
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
10
10
|
|
|
11
11
|
## Current standing
|
|
12
12
|
|
|
@@ -18,15 +18,15 @@ as coverage.
|
|
|
18
18
|
| Deleted upstream identifiers | #20, #273 |
|
|
19
19
|
|
|
20
20
|
The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
|
|
21
|
-
|
|
22
|
-
hook
|
|
23
|
-
|
|
21
|
+
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.
|
|
24
24
|
|
|
25
25
|
## Traceable evidence
|
|
26
26
|
|
|
27
27
|
| Executable evidence | Referenced IDs | WG17 cases |
|
|
28
28
|
| --- | ---: | --- |
|
|
29
|
-
| [complete offline executable matrix](../run-wg17
|
|
29
|
+
| [complete offline executable matrix](../run-wg17.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
|
|
30
30
|
|
|
31
31
|
The evidence groups overlap. Their union is **366** active cases:
|
|
32
32
|
#1–#19, #21–#272, #274–#368.
|
|
@@ -37,6 +37,6 @@ None.
|
|
|
37
37
|
|
|
38
38
|
## Maintenance
|
|
39
39
|
|
|
40
|
-
1.
|
|
41
|
-
2. Review
|
|
42
|
-
3.
|
|
40
|
+
1. Run `npm run wg17:upgrade` to reconcile the dated fixture with upstream.
|
|
41
|
+
2. Review every new or changed ISO expectation before adding its expected outcome.
|
|
42
|
+
3. Run `npm run test:wg17` and keep this generated status page synchronized.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing",
|
|
3
|
-
"checkedOn": "2026-08-
|
|
3
|
+
"checkedOn": "2026-08-16",
|
|
4
4
|
"protocol": "Each query is read and executed in strict ISO mode; /**/ rows reuse the preceding setup.",
|
|
5
5
|
"cases": [
|
|
6
6
|
{
|
|
@@ -5551,14 +5551,13 @@
|
|
|
5551
5551
|
},
|
|
5552
5552
|
{
|
|
5553
5553
|
"id": 368,
|
|
5554
|
-
"query": "
|
|
5555
|
-
"input": "
|
|
5554
|
+
"query": "writeq(?).",
|
|
5555
|
+
"input": "writeq(?).",
|
|
5556
5556
|
"readCount": 1,
|
|
5557
|
-
"expected": "
|
|
5558
|
-
"
|
|
5559
|
-
"type": "error",
|
|
5560
|
-
"formal": "syntax_error(read_term)"
|
|
5561
|
-
}
|
|
5557
|
+
"expected": "?",
|
|
5558
|
+
"assertion": "upstream"
|
|
5562
5559
|
}
|
|
5563
|
-
]
|
|
5560
|
+
],
|
|
5561
|
+
"sourceRevision": "1.300",
|
|
5562
|
+
"sourceSha256": "d7ca05e90cfafe786d5edca4783e691a356a35508c50b62f5a99bd9057e2cd05"
|
|
5564
5563
|
}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing",
|
|
3
|
-
"checkedOn": "2026-08-
|
|
3
|
+
"checkedOn": "2026-08-16",
|
|
4
4
|
"upstream": {
|
|
5
5
|
"firstId": 1,
|
|
6
6
|
"lastId": 368,
|
|
7
|
-
"deletedIds": [
|
|
7
|
+
"deletedIds": [
|
|
8
|
+
20,
|
|
9
|
+
273
|
|
10
|
+
],
|
|
8
11
|
"activeCases": 366
|
|
9
12
|
},
|
|
10
13
|
"evidence": [
|
|
11
14
|
{
|
|
12
15
|
"name": "complete offline executable matrix",
|
|
13
16
|
"path": "test/conformance/wg17-syntax-cases.json",
|
|
14
|
-
"link": "../run-wg17
|
|
15
|
-
"ids": "all-
|
|
17
|
+
"link": "../run-wg17.mjs",
|
|
18
|
+
"ids": "all-executable"
|
|
16
19
|
}
|
|
17
20
|
]
|
|
18
21
|
}
|
package/test/run-all.mjs
CHANGED
|
@@ -9,14 +9,14 @@ import { runIsoStrict } from './run-iso-strict.mjs';
|
|
|
9
9
|
import { runPlayground } from './run-playground.mjs';
|
|
10
10
|
import { runExamples } from './run-examples.mjs';
|
|
11
11
|
import { runBookExamples } from './run-book-examples.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { runWg17 } from './run-wg17.mjs';
|
|
13
13
|
|
|
14
14
|
const reporter = new TestReporter();
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
runConformance(reporter);
|
|
18
18
|
runIsoStrict(reporter);
|
|
19
|
-
|
|
19
|
+
runWg17(reporter);
|
|
20
20
|
runRegression(reporter);
|
|
21
21
|
await runPlayground(reporter);
|
|
22
22
|
runExamples(reporter);
|
package/test/run-regression.mjs
CHANGED
|
@@ -46,6 +46,8 @@ import { buildConformanceReport, formatConformanceReport } from './run-conforman
|
|
|
46
46
|
import { proofExamples } from './run-examples.mjs';
|
|
47
47
|
import { goalsFromSource } from './goal-metadata.mjs';
|
|
48
48
|
import { renderWg17SyntaxStatus } from '../tools/report-wg17-syntax-coverage.mjs';
|
|
49
|
+
import { parseWg17SyntaxTable } from '../tools/upgrade-wg17.mjs';
|
|
50
|
+
import { matchesUpstreamExpectation } from './run-wg17.mjs';
|
|
49
51
|
|
|
50
52
|
const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
51
53
|
const packageRoot = path.resolve(testRoot, '..');
|
|
@@ -2166,6 +2168,49 @@ function documentationSyncCases() {
|
|
|
2166
2168
|
assertEqual(fs.readFileSync(filename, 'utf8'), renderWg17SyntaxStatus(), 'WG17 syntax status');
|
|
2167
2169
|
},
|
|
2168
2170
|
},
|
|
2171
|
+
{
|
|
2172
|
+
name: 'WG17 upgrader accepts omitted HTML table end tags',
|
|
2173
|
+
run: () => {
|
|
2174
|
+
// HTML permits </td> and </tr> to be omitted. TU Wien uses this
|
|
2175
|
+
// compact form, so the upgrader must not depend on explicit closes.
|
|
2176
|
+
const rows = Array.from({ length: 120 }, (_, index) =>
|
|
2177
|
+
`<tr><td>${index + 1}<td><code>write(${index + 1}).</code><td>ok`).join('\n');
|
|
2178
|
+
const html = `<table><tr><th>#<th>Query<th>Codex${rows}</table>`;
|
|
2179
|
+
const parsed = parseWg17SyntaxTable(html);
|
|
2180
|
+
assertEqual(parsed.length, 120, 'parsed row count');
|
|
2181
|
+
assertEqual(parsed[0].id, 1, 'first id');
|
|
2182
|
+
assertEqual(parsed[0].query, 'write(1).', 'first query');
|
|
2183
|
+
assertEqual(parsed.at(-1).id, 120, 'last id');
|
|
2184
|
+
},
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
name: 'WG17 upgrader normalizes presentation non-breaking spaces',
|
|
2188
|
+
run: () => {
|
|
2189
|
+
const rows = Array.from({ length: 120 }, (_, index) =>
|
|
2190
|
+
`<tr><td>${index + 1}<td>set_prolog_flag( double_quotes,chars).<td>succeeds`).join('\n');
|
|
2191
|
+
const html = `<table><tr><th>#<th>Query<th>Codex${rows}</table>`;
|
|
2192
|
+
const parsed = parseWg17SyntaxTable(html);
|
|
2193
|
+
assertEqual(parsed[0].query, 'set_prolog_flag( double_quotes,chars).', 'normalized query');
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
name: 'WG17 direct upstream assertions are executable without local outcomes',
|
|
2198
|
+
run: () => {
|
|
2199
|
+
assertEqual(matchesUpstreamExpectation('succeeds', { type: 'success', stages: [] }), true, 'succeeds');
|
|
2200
|
+
assertEqual(matchesUpstreamExpectation('fails', { type: 'failure' }), true, 'fails');
|
|
2201
|
+
assertEqual(matchesUpstreamExpectation('waits', { type: 'waits' }), true, 'waits');
|
|
2202
|
+
assertEqual(
|
|
2203
|
+
matchesUpstreamExpectation('syntax err.', { type: 'error', formal: 'syntax_error(read_term)' }),
|
|
2204
|
+
true,
|
|
2205
|
+
'syntax error',
|
|
2206
|
+
);
|
|
2207
|
+
assertEqual(
|
|
2208
|
+
matchesUpstreamExpectation("'a b'", { type: 'success', stages: [{ output: "'a b'", variables: '[]' }] }),
|
|
2209
|
+
true,
|
|
2210
|
+
'observable output',
|
|
2211
|
+
);
|
|
2212
|
+
},
|
|
2213
|
+
},
|
|
2169
2214
|
{
|
|
2170
2215
|
name: 'book builtins match runtime registry',
|
|
2171
2216
|
run: () => assertArrayEqual(bookBuiltinNames(), registeredBuiltinNames(), 'builtins'),
|
|
@@ -0,0 +1,201 @@
|
|
|
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.
|
|
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 ?? 16),
|
|
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 canonicalUpstreamExpected(expected) {
|
|
73
|
+
return String(expected)
|
|
74
|
+
.replace(/[²³°]/g, '')
|
|
75
|
+
.replace(/\u00a0/g, ' ')
|
|
76
|
+
.replace(/[ \t\n]+/g, ' ')
|
|
77
|
+
.trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function observableOutput(actual) {
|
|
81
|
+
if (actual.type !== 'success') return null;
|
|
82
|
+
return actual.stages.map(({ output }) => output).join('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function matchesUpstreamExpectation(expectedText, actual) {
|
|
86
|
+
const expected = canonicalUpstreamExpected(expectedText);
|
|
87
|
+
|
|
88
|
+
if (/^waits$/i.test(expected)) return actual.type === 'waits';
|
|
89
|
+
if (/^succeeds(?:\b|$)/i.test(expected)) return actual.type === 'success';
|
|
90
|
+
if (/^fails(?:\b|$)/i.test(expected)) return actual.type === 'failure';
|
|
91
|
+
|
|
92
|
+
if (/^syntax\s*err\.?$/i.test(expected)) {
|
|
93
|
+
return actual.type === 'error' && /^syntax_error\(/.test(actual.formal ?? '');
|
|
94
|
+
}
|
|
95
|
+
if (/^repr\.\s*err\.?$/i.test(expected)) {
|
|
96
|
+
return actual.type === 'error' && /^representation_error\(/.test(actual.formal ?? '');
|
|
97
|
+
}
|
|
98
|
+
if (/^syntax\/repr\.\s*err\.?$/i.test(expected)) {
|
|
99
|
+
return actual.type === 'error' &&
|
|
100
|
+
/^(?:syntax_error|representation_error)\(/.test(actual.formal ?? '');
|
|
101
|
+
}
|
|
102
|
+
if (/^syntax\s*err\.\/waits$/i.test(expected)) {
|
|
103
|
+
return actual.type === 'waits' ||
|
|
104
|
+
(actual.type === 'error' && /^syntax_error\(/.test(actual.formal ?? ''));
|
|
105
|
+
}
|
|
106
|
+
|
|
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;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function usesWaitMatcher(expectedText) {
|
|
117
|
+
const expected = canonicalUpstreamExpected(expectedText);
|
|
118
|
+
return /^waits$/i.test(expected);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function compactTestText(value, maximum) {
|
|
122
|
+
const text = String(value ?? '')
|
|
123
|
+
.replace(/\r/g, '\\r')
|
|
124
|
+
.replace(/\n/g, '\\n')
|
|
125
|
+
.replace(/\t/g, '\\t')
|
|
126
|
+
.replace(/[ ]+/g, ' ')
|
|
127
|
+
.trim();
|
|
128
|
+
if (text.length <= maximum) return text;
|
|
129
|
+
return `${text.slice(0, maximum - 1)}…`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function wg17TestDescription(item) {
|
|
133
|
+
const query = compactTestText(item.query ?? item.input, 56);
|
|
134
|
+
const expected = compactTestText(item.expected, 28);
|
|
135
|
+
return `#${item.id} ${query} -> ${expected}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
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
|
+
}
|
|
150
|
+
|
|
151
|
+
const actual = usesWaitMatcher(item.expected) ? executeWait(item) : executeFinite(item);
|
|
152
|
+
if (!matchesUpstreamExpectation(item.expected, actual)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`WG17 #${item.id} (${item.expected})\n` +
|
|
155
|
+
`upstream expectation did not match\n` +
|
|
156
|
+
`actual ${JSON.stringify(actual)}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readWg17SyntaxFixture() {
|
|
162
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
163
|
+
if (!Array.isArray(fixture.cases) || fixture.cases.length === 0) {
|
|
164
|
+
throw new Error('WG17 syntax fixture has no cases');
|
|
165
|
+
}
|
|
166
|
+
const ids = new Set();
|
|
167
|
+
for (const item of fixture.cases) {
|
|
168
|
+
if (!Number.isInteger(item.id) || ids.has(item.id)) {
|
|
169
|
+
throw new Error(`invalid or duplicate WG17 syntax id #${item.id}`);
|
|
170
|
+
}
|
|
171
|
+
ids.add(item.id);
|
|
172
|
+
}
|
|
173
|
+
return fixture;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function runWg17Syntax(reporter = new TestReporter()) {
|
|
177
|
+
const fixture = readWg17SyntaxFixture();
|
|
178
|
+
|
|
179
|
+
reporter.section('WG17 syntax');
|
|
180
|
+
for (const item of fixture.cases) {
|
|
181
|
+
reporter.test(wg17TestDescription(item), () => assertOutcome(item));
|
|
182
|
+
}
|
|
183
|
+
reporter.sectionTotal('WG17 syntax');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const suites = [runWg17Syntax];
|
|
187
|
+
|
|
188
|
+
export function runWg17(reporter = new TestReporter()) {
|
|
189
|
+
for (const runSuite of suites) runSuite(reporter);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isMainModule(import.meta.url)) {
|
|
193
|
+
const reporter = new TestReporter();
|
|
194
|
+
try {
|
|
195
|
+
runWg17(reporter);
|
|
196
|
+
reporter.totalLine();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
process.stderr.write(`${error?.stack ?? error}\n`);
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -7060,7 +7060,9 @@ syntax. Separate corpora cover expected errors, warnings, and proofs:
|
|
|
7060
7060
|
```sh
|
|
7061
7061
|
npm run test:conformance
|
|
7062
7062
|
npm run test:iso-strict
|
|
7063
|
-
npm run test:wg17
|
|
7063
|
+
npm run test:wg17
|
|
7064
|
+
# Refresh the vendored TU Wien WG17 inventory when upstream changes:
|
|
7065
|
+
npm run wg17:upgrade
|
|
7064
7066
|
node test/run-conformance-report.mjs
|
|
7065
7067
|
```
|
|
7066
7068
|
|
|
@@ -7069,8 +7071,9 @@ Part 1 conformance audit. It distinguishes implemented/tested families from
|
|
|
7069
7071
|
requirements whose normative `shall` clauses, option combinations, or error
|
|
7070
7072
|
precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
|
|
7071
7073
|
maps language families to representative executable cases.
|
|
7072
|
-
`test/conformance/WG17-SYNTAX-STATUS.md` separately traces
|
|
7073
|
-
upstream syntax cases
|
|
7074
|
+
`test/conformance/WG17-SYNTAX-STATUS.md` separately traces the vendored active
|
|
7075
|
+
upstream syntax cases. Reviewed cases can pin exact strict-reader outcomes, while
|
|
7076
|
+
newly upgraded cases execute directly against the upstream Codex expectation.
|
|
7074
7077
|
|
|
7075
7078
|
The complete suite must pass before release. The file-based conformance corpus
|
|
7076
7079
|
contains 791 cases, including 386 focused ISO
|
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
10
|
const manifestPath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-coverage.json');
|
|
11
11
|
const statusPath = path.join(packageRoot, 'test', 'conformance', 'WG17-SYNTAX-STATUS.md');
|
|
12
|
+
const fixturePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-cases.json');
|
|
12
13
|
|
|
13
14
|
export function readWg17SyntaxCoverage() {
|
|
14
15
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -25,10 +26,12 @@ export function readWg17SyntaxCoverage() {
|
|
|
25
26
|
const evidenceEntries = [];
|
|
26
27
|
for (const evidence of manifest.evidence) {
|
|
27
28
|
if (!evidence.name || !evidence.path || !evidence.link ||
|
|
28
|
-
(!Array.isArray(evidence.ids) &&
|
|
29
|
+
(!Array.isArray(evidence.ids) && !['all-active', 'all-reviewed', 'all-executable'].includes(evidence.ids))) {
|
|
29
30
|
throw new Error('invalid WG17 evidence entry');
|
|
30
31
|
}
|
|
31
|
-
const evidenceIds = evidence.ids === 'all-active' ? activeIds :
|
|
32
|
+
const evidenceIds = evidence.ids === 'all-active' ? activeIds :
|
|
33
|
+
['all-reviewed', 'all-executable'].includes(evidence.ids)
|
|
34
|
+
? executableWg17Ids(evidence.path, active) : evidence.ids;
|
|
32
35
|
const evidenceFilename = path.join(packageRoot, evidence.path);
|
|
33
36
|
if (!fs.existsSync(evidenceFilename)) throw new Error(`missing WG17 evidence file ${evidence.path}`);
|
|
34
37
|
const referenced = referencedWg17Ids(fs.readFileSync(evidenceFilename, 'utf8'));
|
|
@@ -52,6 +55,16 @@ export function readWg17SyntaxCoverage() {
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
|
|
59
|
+
function executableWg17Ids(relativePath, active) {
|
|
60
|
+
const filename = path.join(packageRoot, relativePath);
|
|
61
|
+
if (path.resolve(filename) !== path.resolve(fixturePath)) return [];
|
|
62
|
+
const fixture = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
63
|
+
return fixture.cases
|
|
64
|
+
.filter((item) => typeof item.expected === 'string' && item.expected.length > 0 && active.has(item.id))
|
|
65
|
+
.map(({ id }) => id);
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
function referencedWg17Ids(source) {
|
|
56
69
|
const ids = new Set();
|
|
57
70
|
for (const match of source.matchAll(/#(\d+)(?:-(\d+))?/g)) {
|
|
@@ -65,6 +78,10 @@ function referencedWg17Ids(source) {
|
|
|
65
78
|
|
|
66
79
|
export function renderWg17SyntaxStatus() {
|
|
67
80
|
const { manifest, evidenceEntries, activeIds, coveredIds, untracedIds } = readWg17SyntaxCoverage();
|
|
81
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
82
|
+
const waits = fixture.cases.filter((item) =>
|
|
83
|
+
item.outcome?.type === 'waits' || (item.outcome == null && /^waits$/i.test(item.expected ?? ''))).length;
|
|
84
|
+
const direct = fixture.cases.filter((item) => item.outcome == null).length;
|
|
68
85
|
const percentage = (100 * coveredIds.length / activeIds.length).toFixed(1);
|
|
69
86
|
const evidenceRows = evidenceEntries.map((evidence) =>
|
|
70
87
|
`| [${evidence.name}](${evidence.link}) | ${evidence.ids.length} | ${formatRanges(evidence.ids)} |`);
|
|
@@ -74,10 +91,10 @@ export function renderWg17SyntaxStatus() {
|
|
|
74
91
|
Source: [Conformity Testing I: Syntax](${manifest.source})
|
|
75
92
|
Upstream inventory checked: ${manifest.checkedOn}
|
|
76
93
|
|
|
77
|
-
This ledger counts an upstream case
|
|
78
|
-
expected ISO disposition
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
81
98
|
|
|
82
99
|
## Current standing
|
|
83
100
|
|
|
@@ -89,9 +106,9 @@ as coverage.
|
|
|
89
106
|
| Deleted upstream identifiers | ${formatRanges(manifest.upstream.deletedIds)} |
|
|
90
107
|
|
|
91
108
|
The matrix runs in strict ISO stream-reader mode as part of \`npm test\`. The
|
|
92
|
-
|
|
93
|
-
hook
|
|
94
|
-
|
|
109
|
+
${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.
|
|
95
112
|
|
|
96
113
|
## Traceable evidence
|
|
97
114
|
|
|
@@ -108,9 +125,9 @@ ${untracedIds.length === 0 ? 'None.' : `${formatRanges(untracedIds)}.`}
|
|
|
108
125
|
|
|
109
126
|
## Maintenance
|
|
110
127
|
|
|
111
|
-
1.
|
|
112
|
-
2. Review
|
|
113
|
-
3.
|
|
128
|
+
1. Run \`npm run wg17:upgrade\` to reconcile the dated fixture with upstream.
|
|
129
|
+
2. Review every new or changed ISO expectation before adding its expected outcome.
|
|
130
|
+
3. Run \`npm run test:wg17\` and keep this generated status page synchronized.
|
|
114
131
|
`;
|
|
115
132
|
}
|
|
116
133
|
|
|
@@ -132,7 +149,7 @@ if (process.argv[1] != null && path.resolve(process.argv[1]) === fileURLToPath(i
|
|
|
132
149
|
if (process.argv.includes('--check')) {
|
|
133
150
|
const current = fs.readFileSync(statusPath, 'utf8');
|
|
134
151
|
if (current !== rendered) {
|
|
135
|
-
process.stderr.write('WG17 syntax status is stale; run npm run report:wg17
|
|
152
|
+
process.stderr.write('WG17 syntax status is stale; run npm run report:wg17 and update the file.\n');
|
|
136
153
|
process.exitCode = 1;
|
|
137
154
|
}
|
|
138
155
|
} else {
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Refresh the vendored WG17 conformity fixtures from their public upstream
|
|
3
|
+
// tables. Normal test runs remain fully offline and deterministic.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const syntaxSource = 'https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing';
|
|
11
|
+
const syntaxFixturePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-cases.json');
|
|
12
|
+
const syntaxCoveragePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-coverage.json');
|
|
13
|
+
const syntaxStatusPath = path.join(packageRoot, 'test', 'conformance', 'WG17-SYNTAX-STATUS.md');
|
|
14
|
+
|
|
15
|
+
const namedEntities = new Map([
|
|
16
|
+
['amp', '&'], ['lt', '<'], ['gt', '>'], ['quot', '"'], ['apos', "'"],
|
|
17
|
+
['nbsp', '\u00a0'], ['ndash', '–'], ['mdash', '—'], ['minus', '−'],
|
|
18
|
+
['hellip', '…'], ['middot', '·'], ['times', '×'], ['laquo', '«'], ['raquo', '»'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function decodeHtmlEntities(text) {
|
|
22
|
+
return text.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z][a-z0-9]+);/gi, (whole, entity) => {
|
|
23
|
+
if (entity[0] === '#') {
|
|
24
|
+
const hex = entity[1]?.toLowerCase() === 'x';
|
|
25
|
+
const value = Number.parseInt(entity.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
26
|
+
if (Number.isInteger(value) && value >= 0 && value <= 0x10ffff) return String.fromCodePoint(value);
|
|
27
|
+
return whole;
|
|
28
|
+
}
|
|
29
|
+
return namedEntities.get(entity.toLowerCase()) ?? whole;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withoutPresentationMarkup(html) {
|
|
34
|
+
return html
|
|
35
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
36
|
+
.replace(/<sup\b[^>]*>[\s\S]*?<\/sup>/gi, '')
|
|
37
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
38
|
+
.replace(/<\/(?:p|div|pre|li|blockquote)>/gi, '\n')
|
|
39
|
+
.replace(/<(?:p|div|pre|li|blockquote)\b[^>]*>/gi, '')
|
|
40
|
+
.replace(/<[^>]+>/g, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function htmlCellText(html) {
|
|
44
|
+
return decodeHtmlEntities(withoutPresentationMarkup(html))
|
|
45
|
+
.replace(/\r\n?/g, '\n')
|
|
46
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deletedRow(rowHtml, idCellHtml) {
|
|
51
|
+
return /<(?:del|s|strike)\b/i.test(idCellHtml) ||
|
|
52
|
+
/class\s*=\s*["'][^"']*\b(?:deleted|obsolete|removed)\b/i.test(rowHtml);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function htmlTableRows(html) {
|
|
56
|
+
// TU Wien intentionally serves very small, old-style HTML. In valid HTML,
|
|
57
|
+
// </td> and </tr> are optional, and the conformity page currently relies
|
|
58
|
+
// on that. Do not require explicit closing tags here: split on start tags
|
|
59
|
+
// and let the next cell/row start imply the end of the previous one.
|
|
60
|
+
const rowStarts = [...html.matchAll(/<tr\b[^>]*>/gi)];
|
|
61
|
+
const rows = [];
|
|
62
|
+
for (let rowIndex = 0; rowIndex < rowStarts.length; rowIndex++) {
|
|
63
|
+
const rowStart = rowStarts[rowIndex];
|
|
64
|
+
const bodyStart = rowStart.index + rowStart[0].length;
|
|
65
|
+
const nextRow = rowStarts[rowIndex + 1]?.index ?? html.length;
|
|
66
|
+
const explicitEnd = html.slice(bodyStart, nextRow).search(/<\/tr\s*>/i);
|
|
67
|
+
const rowEnd = explicitEnd < 0 ? nextRow : bodyStart + explicitEnd;
|
|
68
|
+
const rowHtml = html.slice(rowStart.index, rowEnd);
|
|
69
|
+
const body = html.slice(bodyStart, rowEnd);
|
|
70
|
+
const cellStarts = [...body.matchAll(/<t[dh]\b[^>]*>/gi)];
|
|
71
|
+
const cells = [];
|
|
72
|
+
for (let cellIndex = 0; cellIndex < cellStarts.length; cellIndex++) {
|
|
73
|
+
const cellStart = cellStarts[cellIndex];
|
|
74
|
+
const cellBodyStart = cellStart.index + cellStart[0].length;
|
|
75
|
+
const cellEnd = cellStarts[cellIndex + 1]?.index ?? body.length;
|
|
76
|
+
cells.push(body.slice(cellBodyStart, cellEnd));
|
|
77
|
+
}
|
|
78
|
+
rows.push({ rowHtml, cells });
|
|
79
|
+
}
|
|
80
|
+
return rows;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseWg17SyntaxTable(html) {
|
|
84
|
+
const cases = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const { rowHtml, cells } of htmlTableRows(html)) {
|
|
87
|
+
if (cells.length < 3 || deletedRow(rowHtml, cells[0])) continue;
|
|
88
|
+
|
|
89
|
+
const idText = htmlCellText(cells[0]).replace(/^#\s*/, '');
|
|
90
|
+
const idMatch = idText.match(/^(\d+)$/);
|
|
91
|
+
if (idMatch == null) continue;
|
|
92
|
+
const id = Number(idMatch[1]);
|
|
93
|
+
if (seen.has(id)) throw new Error(`duplicate active WG17 syntax id #${id} in upstream table`);
|
|
94
|
+
|
|
95
|
+
// TU Wien uses non-breaking spaces for table presentation/indentation.
|
|
96
|
+
// They are not part of the Prolog source being specified, so normalize
|
|
97
|
+
// them to ordinary spaces before snapshotting/comparing rows.
|
|
98
|
+
const query = htmlCellText(cells[1]).replace(/\u00a0/g, ' ');
|
|
99
|
+
const expected = htmlCellText(cells[2]).replace(/\u00a0/g, ' ').replace(/[ \t\n]+/g, ' ').trim();
|
|
100
|
+
if (query.length === 0 || expected.length === 0) continue;
|
|
101
|
+
cases.push({ id, query, expected });
|
|
102
|
+
seen.add(id);
|
|
103
|
+
}
|
|
104
|
+
if (cases.length < 100) {
|
|
105
|
+
const trCount = [...html.matchAll(/<tr\b/gi)].length;
|
|
106
|
+
const tdCount = [...html.matchAll(/<t[dh]\b/gi)].length;
|
|
107
|
+
throw new Error(
|
|
108
|
+
`only ${cases.length} WG17 syntax rows were found ` +
|
|
109
|
+
`(saw ${trCount} row starts and ${tdCount} cell starts); upstream HTML format may have changed`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return cases;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function canonicalQuery(query) {
|
|
116
|
+
return String(query).replace(/\r\n?/g, '\n').replace(/\u00a0/g, ' ').trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
function canonicalExpected(expected) {
|
|
121
|
+
return String(expected).replace(/\s+/g, ' ').trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isLayoutStart(source, index) {
|
|
125
|
+
if (index >= source.length) return true;
|
|
126
|
+
const ch = source[index];
|
|
127
|
+
return /\s/.test(ch) || ch === '%' || (ch === '/' && source[index + 1] === '*');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function firstTermEnd(source) {
|
|
131
|
+
let quote = null;
|
|
132
|
+
let lineComment = false;
|
|
133
|
+
let blockComment = false;
|
|
134
|
+
let depth = 0;
|
|
135
|
+
|
|
136
|
+
for (let index = 0; index < source.length; index++) {
|
|
137
|
+
const ch = source[index];
|
|
138
|
+
const next = source[index + 1];
|
|
139
|
+
|
|
140
|
+
if (lineComment) {
|
|
141
|
+
if (ch === '\n') lineComment = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (blockComment) {
|
|
145
|
+
if (ch === '*' && next === '/') {
|
|
146
|
+
blockComment = false;
|
|
147
|
+
index++;
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (quote != null) {
|
|
152
|
+
if (ch === '\\') {
|
|
153
|
+
index++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (ch === quote && next === quote) {
|
|
157
|
+
index++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === quote) quote = null;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (ch === '%') {
|
|
165
|
+
lineComment = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === '/' && next === '*') {
|
|
169
|
+
blockComment = true;
|
|
170
|
+
index++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === "'" && /\d/.test(source[index - 1] ?? '')) {
|
|
174
|
+
// Character-code constant such as 0'. or 0'\\n: apostrophe is not a
|
|
175
|
+
// quoted-atom delimiter. Skip the character (or escaped character).
|
|
176
|
+
if (next === '\\') index += 2;
|
|
177
|
+
else index++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
181
|
+
quote = ch;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
185
|
+
depth++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
189
|
+
if (depth > 0) depth--;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (ch === '.' && depth === 0 && isLayoutStart(source, index + 1)) return index + 1;
|
|
193
|
+
}
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function skipLayoutAndComments(source, start = 0) {
|
|
198
|
+
let index = start;
|
|
199
|
+
while (index < source.length) {
|
|
200
|
+
if (/\s/.test(source[index])) {
|
|
201
|
+
index++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (source[index] === '%') {
|
|
205
|
+
const newline = source.indexOf('\n', index + 1);
|
|
206
|
+
if (newline < 0) return source.length;
|
|
207
|
+
index = newline + 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (source[index] === '/' && source[index + 1] === '*') {
|
|
211
|
+
const end = source.indexOf('*/', index + 2);
|
|
212
|
+
if (end < 0) return index;
|
|
213
|
+
index = end + 2;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
return index;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Return the first complete Prolog term, without its terminating full stop.
|
|
222
|
+
// This is used only to reconstruct the setup denoted by upstream /**/ rows.
|
|
223
|
+
export function firstPrologTerm(source) {
|
|
224
|
+
const start = skipLayoutAndComments(source, 0);
|
|
225
|
+
const end = firstTermEnd(source.slice(start));
|
|
226
|
+
if (end < 0) return source.slice(start).trim().replace(/\.$/, '').trim();
|
|
227
|
+
return source.slice(start, start + end - 1).trim();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function countTopLevelTerms(source) {
|
|
231
|
+
let index = 0;
|
|
232
|
+
let count = 0;
|
|
233
|
+
while (true) {
|
|
234
|
+
index = skipLayoutAndComments(source, index);
|
|
235
|
+
if (index >= source.length) break;
|
|
236
|
+
const end = firstTermEnd(source.slice(index));
|
|
237
|
+
count++;
|
|
238
|
+
if (end < 0) break;
|
|
239
|
+
index += end;
|
|
240
|
+
}
|
|
241
|
+
return Math.max(1, count);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function setupInput(query, precedingBaseQuery) {
|
|
245
|
+
if (!query.includes('/**/')) return query;
|
|
246
|
+
if (precedingBaseQuery == null) throw new Error(`WG17 query uses /**/ without a preceding setup: ${query}`);
|
|
247
|
+
const setup = firstPrologTerm(precedingBaseQuery);
|
|
248
|
+
if (setup.length === 0) throw new Error(`cannot derive WG17 setup from: ${precedingBaseQuery}`);
|
|
249
|
+
const tail = query.replace('/**/', '');
|
|
250
|
+
return `(catch((${setup}), _, true) -> true ; true).\n${tail}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function reconcileSyntaxCases(upstream, previous) {
|
|
254
|
+
const previousById = new Map(previous.cases.map((item) => [item.id, item]));
|
|
255
|
+
const nextCases = [];
|
|
256
|
+
const added = [];
|
|
257
|
+
const changed = [];
|
|
258
|
+
let precedingBaseQuery = null;
|
|
259
|
+
|
|
260
|
+
for (const row of upstream) {
|
|
261
|
+
const old = previousById.get(row.id);
|
|
262
|
+
const same = old != null &&
|
|
263
|
+
canonicalQuery(old.query) === canonicalQuery(row.query) &&
|
|
264
|
+
canonicalExpected(old.expected) === canonicalExpected(row.expected);
|
|
265
|
+
|
|
266
|
+
if (same) {
|
|
267
|
+
nextCases.push(old);
|
|
268
|
+
} else {
|
|
269
|
+
const input = setupInput(row.query, precedingBaseQuery);
|
|
270
|
+
const item = {
|
|
271
|
+
id: row.id,
|
|
272
|
+
query: row.query,
|
|
273
|
+
input,
|
|
274
|
+
readCount: countTopLevelTerms(input),
|
|
275
|
+
expected: row.expected,
|
|
276
|
+
assertion: 'upstream',
|
|
277
|
+
};
|
|
278
|
+
nextCases.push(item);
|
|
279
|
+
if (old == null) added.push(row.id);
|
|
280
|
+
else changed.push(row.id);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!row.query.includes('/**/')) precedingBaseQuery = row.query;
|
|
284
|
+
previousById.delete(row.id);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
cases: nextCases,
|
|
289
|
+
added,
|
|
290
|
+
changed,
|
|
291
|
+
removed: [...previousById.keys()].sort((a, b) => a - b),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function inventoryFromCases(cases) {
|
|
296
|
+
const ids = cases.map(({ id }) => id);
|
|
297
|
+
if (ids.length === 0) throw new Error('WG17 syntax inventory is empty');
|
|
298
|
+
const firstId = Math.min(...ids);
|
|
299
|
+
const lastId = Math.max(...ids);
|
|
300
|
+
const active = new Set(ids);
|
|
301
|
+
const deletedIds = [];
|
|
302
|
+
for (let id = firstId; id <= lastId; id++) if (!active.has(id)) deletedIds.push(id);
|
|
303
|
+
return { firstId, lastId, deletedIds, activeCases: cases.length };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sourceRevision(html) {
|
|
307
|
+
const text = htmlCellText(html);
|
|
308
|
+
const revisions = [...text.matchAll(/\brevision\s+([0-9]+(?:\.[0-9]+)*)/gi)];
|
|
309
|
+
return revisions.at(-1)?.[1] ?? null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function dateStamp() {
|
|
313
|
+
return new Date().toISOString().slice(0, 10);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatIdList(ids) {
|
|
317
|
+
return ids.length === 0 ? 'none' : ids.map((id) => `#${id}`).join(', ');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function readSource(source) {
|
|
321
|
+
if (/^https?:\/\//i.test(source)) {
|
|
322
|
+
const response = await fetch(source, {
|
|
323
|
+
headers: { 'user-agent': 'EyeProlog-WG17-upgrader/1' },
|
|
324
|
+
redirect: 'follow',
|
|
325
|
+
});
|
|
326
|
+
if (!response.ok) throw new Error(`WG17 fetch failed: ${response.status} ${response.statusText}`);
|
|
327
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
328
|
+
const header = response.headers.get('content-type') ?? '';
|
|
329
|
+
return { bytes, html: decodeDocument(bytes, header) };
|
|
330
|
+
}
|
|
331
|
+
const filename = path.resolve(source);
|
|
332
|
+
const bytes = new Uint8Array(fs.readFileSync(filename));
|
|
333
|
+
return { bytes, html: decodeDocument(bytes, '') };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function decodeDocument(bytes, contentType = '') {
|
|
337
|
+
const prefix = Buffer.from(bytes.subarray(0, Math.min(bytes.length, 8192))).toString('latin1');
|
|
338
|
+
const headerCharset = contentType.match(/charset\s*=\s*["']?([^;"'\s]+)/i)?.[1];
|
|
339
|
+
const metaCharset = prefix.match(/<meta[^>]+charset\s*=\s*["']?([^"'\s/>;]+)/i)?.[1] ??
|
|
340
|
+
prefix.match(/<meta[^>]+content\s*=\s*["'][^"']*charset\s*=\s*([^;"'\s>]+)/i)?.[1];
|
|
341
|
+
const label = headerCharset ?? metaCharset ?? 'utf-8';
|
|
342
|
+
try {
|
|
343
|
+
return new TextDecoder(label).decode(bytes);
|
|
344
|
+
} catch (_) {
|
|
345
|
+
return new TextDecoder('windows-1252').decode(bytes);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function parseArgs(argv) {
|
|
350
|
+
const options = { check: false, source: syntaxSource };
|
|
351
|
+
for (let index = 0; index < argv.length; index++) {
|
|
352
|
+
const arg = argv[index];
|
|
353
|
+
if (arg === '--check') options.check = true;
|
|
354
|
+
else if (arg === '--source') {
|
|
355
|
+
if (argv[index + 1] == null) throw new Error('--source requires a URL or filename');
|
|
356
|
+
options.source = argv[++index];
|
|
357
|
+
} else if (arg === '--help' || arg === '-h') options.help = true;
|
|
358
|
+
else throw new Error(`unknown option ${arg}`);
|
|
359
|
+
}
|
|
360
|
+
return options;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function printHelp() {
|
|
364
|
+
process.stdout.write(`Usage: npm run wg17:upgrade -- [--check] [--source URL_OR_FILE]\n\n` +
|
|
365
|
+
`Refreshes the vendored WG17 conformity tests from the TU Wien table.\n` +
|
|
366
|
+
`New or changed rows are executable immediately against the upstream\n` +
|
|
367
|
+
`Codex expectation; existing reviewed exact outcomes remain pinned.\n`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function upgradeWg17({ check = false, source = syntaxSource } = {}) {
|
|
371
|
+
const previous = JSON.parse(fs.readFileSync(syntaxFixturePath, 'utf8'));
|
|
372
|
+
const { bytes, html } = await readSource(source);
|
|
373
|
+
const upstream = parseWg17SyntaxTable(html);
|
|
374
|
+
const reconciliation = reconcileSyntaxCases(upstream, previous);
|
|
375
|
+
const semanticChanges = reconciliation.added.length + reconciliation.changed.length + reconciliation.removed.length;
|
|
376
|
+
|
|
377
|
+
process.stdout.write(`WG17 syntax: ${upstream.length} active upstream cases\n`);
|
|
378
|
+
process.stdout.write(` added: ${formatIdList(reconciliation.added)}\n`);
|
|
379
|
+
process.stdout.write(` changed: ${formatIdList(reconciliation.changed)}\n`);
|
|
380
|
+
process.stdout.write(` removed: ${formatIdList(reconciliation.removed)}\n`);
|
|
381
|
+
|
|
382
|
+
if (check) {
|
|
383
|
+
if (semanticChanges > 0) {
|
|
384
|
+
process.stderr.write('WG17 snapshot is stale; run npm run wg17:upgrade.\n');
|
|
385
|
+
process.exitCode = 1;
|
|
386
|
+
return { changed: true, ...reconciliation };
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write('WG17 snapshot matches the upstream test inventory.\n');
|
|
389
|
+
return { changed: false, ...reconciliation };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const checkedOn = dateStamp();
|
|
393
|
+
const fixture = {
|
|
394
|
+
...previous,
|
|
395
|
+
source: syntaxSource,
|
|
396
|
+
checkedOn,
|
|
397
|
+
sourceRevision: sourceRevision(html),
|
|
398
|
+
sourceSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
399
|
+
protocol: 'Each query is read and executed in strict ISO mode; /**/ rows reuse the preceding setup.',
|
|
400
|
+
cases: reconciliation.cases,
|
|
401
|
+
};
|
|
402
|
+
fs.writeFileSync(syntaxFixturePath, `${JSON.stringify(fixture, null, 2)}\n`);
|
|
403
|
+
|
|
404
|
+
const coverage = JSON.parse(fs.readFileSync(syntaxCoveragePath, 'utf8'));
|
|
405
|
+
coverage.source = syntaxSource;
|
|
406
|
+
coverage.checkedOn = checkedOn;
|
|
407
|
+
coverage.upstream = inventoryFromCases(fixture.cases);
|
|
408
|
+
for (const evidence of coverage.evidence ?? []) {
|
|
409
|
+
if (['all-active', 'all-reviewed'].includes(evidence.ids)) evidence.ids = 'all-executable';
|
|
410
|
+
if (evidence.path === 'test/conformance/wg17-syntax-cases.json') evidence.link = '../run-wg17.mjs';
|
|
411
|
+
}
|
|
412
|
+
fs.writeFileSync(syntaxCoveragePath, `${JSON.stringify(coverage, null, 2)}\n`);
|
|
413
|
+
|
|
414
|
+
// Generate status after the fixture/manifest are synchronized.
|
|
415
|
+
const { renderWg17SyntaxStatus } = await import('./report-wg17-syntax-coverage.mjs');
|
|
416
|
+
fs.writeFileSync(syntaxStatusPath, renderWg17SyntaxStatus());
|
|
417
|
+
|
|
418
|
+
const upstreamAssertions = fixture.cases
|
|
419
|
+
.filter((item) => item.outcome == null)
|
|
420
|
+
.map(({ id }) => id);
|
|
421
|
+
process.stdout.write(`Updated WG17 snapshot (${fixture.cases.length} cases).\n`);
|
|
422
|
+
if (upstreamAssertions.length > 0) {
|
|
423
|
+
process.stdout.write(
|
|
424
|
+
`Direct upstream assertions used by test:wg17: ${formatIdList(upstreamAssertions)}\n`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return { changed: semanticChanges > 0, upstreamAssertions, ...reconciliation };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const isMain = process.argv[1] != null && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
|
|
431
|
+
if (isMain) {
|
|
432
|
+
try {
|
|
433
|
+
const options = parseArgs(process.argv.slice(2));
|
|
434
|
+
if (options.help) printHelp();
|
|
435
|
+
else await upgradeWg17(options);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
process.stderr.write(`${error?.stack ?? error}\n`);
|
|
438
|
+
process.exitCode = 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
package/why-eyeprolog.md
CHANGED
|
@@ -20,7 +20,7 @@ syntax.
|
|
|
20
20
|
EyeProlog implements the Part 1 core together with Technical Corrigenda 1, 2,
|
|
21
21
|
and 3, Part 2 modules, and the Part 3 definite clause grammar specification.
|
|
22
22
|
Its executable conformance matrix and tests document the supported
|
|
23
|
-
behavior, including
|
|
23
|
+
behavior, including an executable trace of the vendored active WG17 syntax
|
|
24
24
|
cases. This is extensive implementation evidence, not certification by an
|
|
25
25
|
independent standards body.
|
|
26
26
|
|