eyeprolog 1.2.24 → 1.2.26
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/examples/output/access-control-policy.pl +2 -0
- package/examples/output/d3-group.pl +2 -2
- package/examples/output/map-four-color-search.pl +1 -0
- package/examples/output/partial-evaluator.pl +3 -0
- package/examples/output/weighted-interval-scheduling.pl +4 -0
- package/examples/proof/access-control-policy.pl +158 -0
- package/examples/proof/d3-group.pl +17 -17
- package/examples/proof/partial-evaluator.pl +359 -0
- package/examples/proof/weighted-interval-scheduling.pl +8388 -0
- package/package.json +3 -1
- package/src/iso.js +31 -30
- package/src/parser.js +238 -77
- package/src/program.js +2 -1
- package/src/repl.js +10 -32
- package/src/syntax-scan.js +54 -0
- package/src/write.js +19 -2
- package/test/conformance/ISO-COMPLIANCE.md +6 -5
- package/test/conformance/WG17-SYNTAX-STATUS.md +42 -0
- package/test/conformance/cases/iso/wg17_syntax_high_risk.pl +5 -1
- package/test/conformance/expected/iso/wg17_syntax_high_risk.pl +1 -0
- package/test/conformance/wg17-syntax-cases.json +5564 -0
- package/test/conformance/wg17-syntax-coverage.json +18 -0
- package/test/run-all.mjs +2 -0
- package/test/run-regression.mjs +96 -1
- package/test/run-wg17-syntax.mjs +106 -0
- package/tools/report-wg17-syntax-coverage.mjs +141 -0
package/src/repl.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readSync } from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { formalErrorTerm } from './iso.js';
|
|
7
|
+
import { characterCodeConstantEnd, quotedEscapeEnd } from './syntax-scan.js';
|
|
7
8
|
|
|
8
9
|
const ANSWER_HELP = `
|
|
9
10
|
SPACE, "n" or ";": next solution, if any
|
|
@@ -344,38 +345,6 @@ async function readInteractiveTerm(reader) {
|
|
|
344
345
|
}
|
|
345
346
|
}
|
|
346
347
|
|
|
347
|
-
function quotedEscapeEnd(source, index) {
|
|
348
|
-
const escaped = source[index + 1] ?? '';
|
|
349
|
-
if (!escaped) return index;
|
|
350
|
-
|
|
351
|
-
// A backslash-newline pair is a continuation escape (6.4.2), so the
|
|
352
|
-
// newline is not a literal quoted character. Accept CRLF as the host text
|
|
353
|
-
// representation of the same continuation boundary.
|
|
354
|
-
if (escaped === '\n') return index + 1;
|
|
355
|
-
if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
|
|
356
|
-
|
|
357
|
-
// ISO 6.4.2.1 octal and hexadecimal escapes are terminated by a
|
|
358
|
-
// backslash. Consume that terminator as part of the escape so the REPL
|
|
359
|
-
// scanner does not mistake it for an escape of the following quote.
|
|
360
|
-
if (escaped === 'x') {
|
|
361
|
-
let cursor = index + 2;
|
|
362
|
-
while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
|
|
363
|
-
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
364
|
-
}
|
|
365
|
-
if (/^[0-9]$/.test(escaped)) {
|
|
366
|
-
let cursor = index + 1;
|
|
367
|
-
// Scan all decimal digits here, including 8 and 9. This scanner only
|
|
368
|
-
// locates the end of a candidate quoted escape; the parser remains
|
|
369
|
-
// authoritative and rejects non-octal digits.
|
|
370
|
-
while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
|
|
371
|
-
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Meta escapes, symbolic control escapes, and continuation escapes consume
|
|
375
|
-
// one character after the backslash. The parser performs validity checks.
|
|
376
|
-
return index + 1;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
348
|
function terminalFullStop(source) {
|
|
380
349
|
let quote = null;
|
|
381
350
|
let lineComment = false;
|
|
@@ -411,6 +380,11 @@ function terminalFullStop(source) {
|
|
|
411
380
|
}
|
|
412
381
|
continue;
|
|
413
382
|
}
|
|
383
|
+
const characterCodeEnd = characterCodeConstantEnd(source, i);
|
|
384
|
+
if (characterCodeEnd != null) {
|
|
385
|
+
i = characterCodeEnd;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
414
388
|
if (ch === '%') {
|
|
415
389
|
lineComment = true;
|
|
416
390
|
continue;
|
|
@@ -636,6 +610,10 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
636
610
|
operators,
|
|
637
611
|
variableNames: names,
|
|
638
612
|
maxPriority: valueMaxPriority,
|
|
613
|
+
// A binding value is the right-hand `arg` of =/2. ISO 6.3.3.1 permits
|
|
614
|
+
// current operator atoms in argument and list-element positions without
|
|
615
|
+
// quotes, just as writeq/1 already prints them.
|
|
616
|
+
operatorAtomsAsArgs: true,
|
|
639
617
|
})}`);
|
|
640
618
|
}
|
|
641
619
|
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Lightweight lexical helpers shared by the interactive and ISO stream
|
|
2
|
+
// readers. These locate token boundaries only; parser.js remains responsible
|
|
3
|
+
// for accepting or rejecting the token itself.
|
|
4
|
+
|
|
5
|
+
export function quotedEscapeEnd(source, index) {
|
|
6
|
+
const escaped = source[index + 1] ?? '';
|
|
7
|
+
if (!escaped) return index;
|
|
8
|
+
|
|
9
|
+
if (escaped === '\n') return index + 1;
|
|
10
|
+
if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
|
|
11
|
+
|
|
12
|
+
if (escaped === 'x') {
|
|
13
|
+
let cursor = index + 2;
|
|
14
|
+
while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
|
|
15
|
+
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
16
|
+
}
|
|
17
|
+
if (/^[0-9]$/.test(escaped)) {
|
|
18
|
+
let cursor = index + 1;
|
|
19
|
+
// Include 8 and 9 while finding the boundary. The parser rejects them as
|
|
20
|
+
// non-octal rather than letting the reader split the malformed token.
|
|
21
|
+
while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
|
|
22
|
+
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return index + 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function characterCodeConstantEnd(source, apostropheIndex) {
|
|
29
|
+
if (source[apostropheIndex] !== "'" || source[apostropheIndex - 1] !== '0') return null;
|
|
30
|
+
// The 0 must begin a numeric token. In particular, do not reinterpret the
|
|
31
|
+
// apostrophe in an identifier such as a0'x as character-code notation.
|
|
32
|
+
if (/[A-Za-z0-9_]/.test(source[apostropheIndex - 2] ?? '')) return null;
|
|
33
|
+
|
|
34
|
+
const characterIndex = apostropheIndex + 1;
|
|
35
|
+
const character = source[characterIndex] ?? '';
|
|
36
|
+
if (!character) return apostropheIndex;
|
|
37
|
+
// `0''` is two tokens (0 and the empty atom), and `0'\\\n...'
|
|
38
|
+
// likewise starts a quoted atom containing a continuation. Only `0'''`
|
|
39
|
+
// denotes the character-code constant for an apostrophe.
|
|
40
|
+
if (character === "'" && source[characterIndex + 1] !== "'") return null;
|
|
41
|
+
if (character === '\\' && ['\n', '\r'].includes(source[characterIndex + 1])) return null;
|
|
42
|
+
if (character === '\\') return quotedEscapeEnd(source, characterIndex);
|
|
43
|
+
|
|
44
|
+
// An apostrophe character is doubled in 0''' exactly as it is in a quoted
|
|
45
|
+
// atom. Consume both so neither one opens a quoted-token scan state.
|
|
46
|
+
if (character === "'" && source[characterIndex + 1] === "'") return characterIndex + 1;
|
|
47
|
+
|
|
48
|
+
const firstCode = character.charCodeAt(0);
|
|
49
|
+
if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
|
|
50
|
+
const secondCode = source.charCodeAt(characterIndex + 1);
|
|
51
|
+
if (secondCode >= 0xdc00 && secondCode <= 0xdfff) return characterIndex + 1;
|
|
52
|
+
}
|
|
53
|
+
return characterIndex;
|
|
54
|
+
}
|
package/src/write.js
CHANGED
|
@@ -28,6 +28,7 @@ function atomNeedsQuotes(name) {
|
|
|
28
28
|
if (!name) return true;
|
|
29
29
|
if (name === '[]' || name === '{}') return false;
|
|
30
30
|
if (name === '...') return false;
|
|
31
|
+
if (name.startsWith('/*')) return true;
|
|
31
32
|
if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
|
|
32
33
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
|
|
33
34
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
|
@@ -104,6 +105,7 @@ function writeNumberedVariable(index) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function operatorName(name) {
|
|
108
|
+
if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
|
|
107
109
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return name;
|
|
108
110
|
if (/^[!#$&*+\-./<=>@^~\\;:]+$/.test(name)) return name;
|
|
109
111
|
return quoteAtom(name);
|
|
@@ -142,6 +144,12 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
142
144
|
// `arg` production: an atom that is a current operator is valid there
|
|
143
145
|
// without quoting. Keep lexical exceptions such as `|` quoted.
|
|
144
146
|
if (options.operatorAtomsAsArgs && context === 'argument' && table.has(resolved.name)) return operatorName(resolved.name);
|
|
147
|
+
if (!options.ignoreOps && context !== 'argument' && table.has(resolved.name)) {
|
|
148
|
+
const definitions = table.get(resolved.name);
|
|
149
|
+
const requiresParentheses = definitions.some(({ specifier }) =>
|
|
150
|
+
['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(specifier));
|
|
151
|
+
if (requiresParentheses) return `(${operatorName(resolved.name)})`;
|
|
152
|
+
}
|
|
145
153
|
return writeAtom(resolved.name);
|
|
146
154
|
}
|
|
147
155
|
if (resolved.type === NUMBER) return resolved.name;
|
|
@@ -186,11 +194,20 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
186
194
|
const argumentPriority = specifier === 'fx' ? priority - 1 : priority;
|
|
187
195
|
text = `${token} ${format(resolved.args[0], env, options, table, argumentPriority)}`;
|
|
188
196
|
} else if (specifier === 'xf' || specifier === 'yf') {
|
|
189
|
-
|
|
197
|
+
let argumentPriority = specifier === 'xf' ? priority - 1 : priority;
|
|
198
|
+
const childDefinition = chooseOperator(deref(resolved.args[0], env), table);
|
|
199
|
+
if (childDefinition?.priority === priority &&
|
|
200
|
+
['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(childDefinition.specifier)) {
|
|
201
|
+
argumentPriority = priority - 1;
|
|
202
|
+
}
|
|
190
203
|
text = `${format(resolved.args[0], env, options, table, argumentPriority)} ${token}`;
|
|
191
204
|
} else {
|
|
192
|
-
|
|
205
|
+
let leftPriority = specifier === 'yfx' ? priority : priority - 1;
|
|
193
206
|
const rightPriority = specifier === 'xfy' ? priority : priority - 1;
|
|
207
|
+
const leftDefinition = chooseOperator(deref(resolved.args[0], env), table);
|
|
208
|
+
if (leftDefinition?.priority === priority && ['fx', 'fy'].includes(leftDefinition.specifier)) {
|
|
209
|
+
leftPriority = priority - 1;
|
|
210
|
+
}
|
|
194
211
|
const left = format(resolved.args[0], env, options, table, leftPriority);
|
|
195
212
|
const right = format(resolved.args[1], env, options, table, rightPriority);
|
|
196
213
|
text = resolved.name === ',' ? `${left}, ${right}`
|
|
@@ -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
|
|
21
|
+
| 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete 366-case 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 | `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases,
|
|
33
|
+
| Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete 366-case 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,6 +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-syntax
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
The unified `npm test` gate includes the strict-core suite. Expected conformance
|
|
@@ -88,9 +89,9 @@ ISO/IEC 13211-1 processor” until all of the following are true:
|
|
|
88
89
|
1. every normative Part 1 processor requirement is represented in this ledger;
|
|
89
90
|
2. every `audit` row above has been reduced to explicit pass/not-applicable or
|
|
90
91
|
documented implementation-defined choices;
|
|
91
|
-
3. the
|
|
92
|
-
|
|
93
|
-
explained or fixed;
|
|
92
|
+
3. the external WG17 conversion and variable-name conformity corpora have
|
|
93
|
+
joined the now-complete syntax corpus in strict core mode, with every
|
|
94
|
+
difference explained or fixed;
|
|
94
95
|
4. prescribed modes, errors, side effects, and relevant error precedence for
|
|
95
96
|
every Part 1 built-in have executable coverage;
|
|
96
97
|
5. every implementation-defined/dependent/specific choice required to be
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# WG17 syntax traceability status
|
|
2
|
+
|
|
3
|
+
Source: [Conformity Testing I: Syntax](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing)
|
|
4
|
+
Upstream inventory checked: 2026-08-15
|
|
5
|
+
|
|
6
|
+
This ledger counts an upstream case only when its WG17 identifier, query,
|
|
7
|
+
expected ISO disposition, and observed EyeProlog outcome are stored in the
|
|
8
|
+
offline executable matrix. Semantically similar parser tests are not inferred
|
|
9
|
+
as coverage.
|
|
10
|
+
|
|
11
|
+
## Current standing
|
|
12
|
+
|
|
13
|
+
| Measure | Count |
|
|
14
|
+
| --- | ---: |
|
|
15
|
+
| Active upstream cases | 366 |
|
|
16
|
+
| Executable EyeProlog dispositions | 366 (100.0%) |
|
|
17
|
+
| Not yet traced one-by-one | 0 |
|
|
18
|
+
| Deleted upstream identifiers | #20, #273 |
|
|
19
|
+
|
|
20
|
+
The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
|
|
21
|
+
three upstream `waits` cases are checked through EyeProlog's interactive input
|
|
22
|
+
hook; the other 363 cases are checked for their exact stored
|
|
23
|
+
success output, bindings, failure, or ISO error category.
|
|
24
|
+
|
|
25
|
+
## Traceable evidence
|
|
26
|
+
|
|
27
|
+
| Executable evidence | Referenced IDs | WG17 cases |
|
|
28
|
+
| --- | ---: | --- |
|
|
29
|
+
| [complete offline executable matrix](../run-wg17-syntax.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
|
|
30
|
+
|
|
31
|
+
The evidence groups overlap. Their union is **366** active cases:
|
|
32
|
+
#1–#19, #21–#272, #274–#368.
|
|
33
|
+
|
|
34
|
+
## Untraced upstream identifiers
|
|
35
|
+
|
|
36
|
+
None.
|
|
37
|
+
|
|
38
|
+
## Maintenance
|
|
39
|
+
|
|
40
|
+
1. Refresh the dated fixture when the upstream table changes.
|
|
41
|
+
2. Review any changed ISO expectation before updating an observed snapshot.
|
|
42
|
+
3. Keep this generated status page synchronized in the release gate.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
% High-risk ISO syntax/write regressions, independently derived from
|
|
2
2
|
% ISO/IEC 13211-1 clauses 6.3, 6.4 and 7.10 and cross-checked against the
|
|
3
|
-
% public WG17 conformity-testing syntax cases (#1, #7-10, #14-15, #18, #28-31, #33-34, #301, #315-316).
|
|
3
|
+
% public WG17 conformity-testing syntax cases (#1, #7-10, #14-15, #18, #28-31, #33-34, #299, #301, #315-316).
|
|
4
4
|
|
|
5
5
|
%% goal: wg17_numeric_escape
|
|
6
6
|
wg17_numeric_escape :-
|
|
@@ -21,6 +21,10 @@ wg17_operator_precedence :-
|
|
|
21
21
|
writeq(a*(b+c)), nl,
|
|
22
22
|
writeq((a :- b,c)), nl.
|
|
23
23
|
|
|
24
|
+
%% goal: wg17_spaced_prefix_operator(T)
|
|
25
|
+
wg17_spaced_prefix_operator(T) :-
|
|
26
|
+
(\+ (a,b)) = \+(T).
|
|
27
|
+
|
|
24
28
|
%% goal: wg17_canonical_list
|
|
25
29
|
wg17_canonical_list :-
|
|
26
30
|
write_canonical([a]), nl.
|