eyeprolog 1.2.28 → 1.2.30
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 -0
- package/conformance-report.md +3 -3
- package/examples/output/fast-fourier-transform.pl +1 -1
- package/package.json +1 -1
- package/src/io.js +73 -5
- package/src/iso.js +50 -7
- package/src/lib/clpz.pl +13 -13
- package/src/lib/iso_ext.pl +1 -3
- package/src/lib/lists.pl +1 -1
- package/src/lib/prologue.pl +1 -3
- package/src/parser.js +24 -21
- package/src/repl.js +1 -0
- package/src/solver.js +17 -1
- package/src/standard-library.js +2 -2
- package/src/write.js +6 -3
- package/test/conformance/ISO-COMPLIANCE.md +1 -1
- package/test/conformance/README.md +13 -5
- package/test/conformance/cases/atoms/question_mark_graphic_atom.pl +3 -0
- package/test/conformance/cases/iso/cut_control.pl +20 -0
- package/test/conformance/expected/atoms/question_mark_graphic_atom.pl +1 -0
- package/test/conformance/expected/iso/cut_control.pl +2 -0
- package/test/conformance/expected/iso/operator_atoms_as_arguments.pl +2 -2
- package/test/conformance/expected-errors/syntax/bad_question_variable.txt +1 -1
- package/test/conformance/expected-errors/variables/bare_underscore_name_in_compound_rejected.txt +1 -1
- package/test/conformance/expected-errors/variables/extra_uppercase_in_list.txt +1 -1
- package/test/conformance/expected-errors/variables/question_digit_rejected.txt +1 -1
- package/test/conformance/expected-errors/variables/underscore_named_rejected.txt +1 -1
- package/test/conformance/expected-errors/variables/uppercase_in_body_rejected.txt +1 -1
- package/test/conformance/expected-errors/variables/uppercase_variable_rejected.txt +1 -1
- package/test/conformance/wg17-syntax-cases.json +5 -5
- package/test/fixtures/prologue_extended_quad.pl +2 -2
- package/test/run-regression.mjs +122 -27
- package/the-art-of-eyeprolog.md +23 -10
- package/why-eyeprolog.md +2 -1
- package/test/conformance/errors/variables/bare_underscore_in_list_rejected.pl +0 -1
- package/test/conformance/errors/variables/bare_underscore_rejected.pl +0 -1
- package/test/conformance/expected-errors/variables/bare_underscore_in_list_rejected.txt +0 -1
- package/test/conformance/expected-errors/variables/bare_underscore_rejected.txt +0 -1
package/README.md
CHANGED
|
@@ -104,6 +104,9 @@ modules, DCGs, quads, libraries, proofs, and the other documented extensions.
|
|
|
104
104
|
|
|
105
105
|
The auditable processor-requirement checklist lives in
|
|
106
106
|
[`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md).
|
|
107
|
+
The separate [WG17 syntax ledger](test/conformance/WG17-SYNTAX-STATUS.md)
|
|
108
|
+
records executable dispositions for all 366 active upstream syntax cases
|
|
109
|
+
(100% traced) and runs as part of `npm test`.
|
|
107
110
|
EyeProlog does not yet claim independent certification or closure of every
|
|
108
111
|
normative Part 1 requirement.
|
|
109
112
|
|
package/conformance-report.md
CHANGED
|
@@ -6,7 +6,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
6
6
|
|---|---:|---:|---:|---:|---:|
|
|
7
7
|
| aggregation | 17 | 0 | 0 | 0 | 17 |
|
|
8
8
|
| arithmetic | 38 | 0 | 0 | 0 | 38 |
|
|
9
|
-
| atoms |
|
|
9
|
+
| atoms | 23 | 8 | 0 | 0 | 31 |
|
|
10
10
|
| automatic-tabling | 6 | 0 | 0 | 0 | 6 |
|
|
11
11
|
| builtins | 11 | 0 | 0 | 0 | 11 |
|
|
12
12
|
| context | 11 | 0 | 0 | 0 | 11 |
|
|
@@ -22,5 +22,5 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
22
22
|
| syntax | 12 | 23 | 0 | 0 | 35 |
|
|
23
23
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
24
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
|
-
| variables | 16 |
|
|
26
|
-
| **Total** | **
|
|
25
|
+
| variables | 16 | 7 | 0 | 0 | 23 |
|
|
26
|
+
| **Total** | **484** | **267** | **19** | **21** | **791** |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
fft([0, 1, 2, 3, 4, 5, 6, 7], [n(232, op(
|
|
1
|
+
fft([0, 1, 2, 3, 4, 5, 6, 7], [n(232, op(+, 216, 231)), n(231, op(*, 217, 230)), n(230, op(+, 222, 229)), n(229, op(*, 223, 228)), n(228, op(+, 224, 227)), n(227, op(*, 225, 226)), n(226, a(7)), n(225, w ^ 4), n(224, a(3)), n(223, w ^ 6), n(222, op(+, 218, 221)), n(221, op(*, 219, 220)), n(220, a(5)), n(219, w ^ 4), n(218, a(1)), n(217, w ^ 7), n(216, op(+, 208, 215)), n(215, op(*, 209, 214)), n(214, op(+, 210, 213)), n(213, op(*, 211, 212)), n(212, a(6)), n(211, w ^ 4), n(210, a(2)), n(209, w ^ 6), n(208, op(+, 204, 207)), n(207, op(*, 205, 206)), n(206, a(4)), n(205, w ^ 4), n(204, a(0)), n(203, op(+, 187, 202)), n(202, op(*, 188, 201)), n(201, op(+, 193, 200)), n(200, op(*, 194, 199)), n(199, op(+, 195, 198)), n(198, op(*, 196, 197)), n(197, a(7)), n(196, w ^ 0), n(195, a(3)), n(194, w ^ 4), n(193, op(+, 189, 192)), n(192, op(*, 190, 191)), n(191, a(5)), n(190, w ^ 0), n(189, a(1)), n(188, w ^ 6), n(187, op(+, 179, 186)), n(186, op(*, 180, 185)), n(185, op(+, 181, 184)), n(184, op(*, 182, 183)), n(183, a(6)), n(182, w ^ 0), n(181, a(2)), n(180, w ^ 4), n(179, op(+, 175, 178)), n(178, op(*, 176, 177)), n(177, a(4)), n(176, w ^ 0), n(175, a(0)), n(174, op(+, 158, 173)), n(173, op(*, 159, 172)), n(172, op(+, 164, 171)), n(171, op(*, 165, 170)), n(170, op(+, 166, 169)), n(169, op(*, 167, 168)), n(168, a(7)), n(167, w ^ 4), n(166, a(3)), n(165, w ^ 2), n(164, op(+, 160, 163)), n(163, op(*, 161, 162)), n(162, a(5)), n(161, w ^ 4), n(160, a(1)), n(159, w ^ 5), n(158, op(+, 150, 157)), n(157, op(*, 151, 156)), n(156, op(+, 152, 155)), n(155, op(*, 153, 154)), n(154, a(6)), n(153, w ^ 4), n(152, a(2)), n(151, w ^ 2), n(150, op(+, 146, 149)), n(149, op(*, 147, 148)), n(148, a(4)), n(147, w ^ 4), n(146, a(0)), n(145, op(+, 129, 144)), n(144, op(*, 130, 143)), n(143, op(+, 135, 142)), n(142, op(*, 136, 141)), n(141, op(+, 137, 140)), n(140, op(*, 138, 139)), n(139, a(7)), n(138, w ^ 0), n(137, a(3)), n(136, w ^ 0), n(135, op(+, 131, 134)), n(134, op(*, 132, 133)), n(133, a(5)), n(132, w ^ 0), n(131, a(1)), n(130, w ^ 4), n(129, op(+, 121, 128)), n(128, op(*, 122, 127)), n(127, op(+, 123, 126)), n(126, op(*, 124, 125)), n(125, a(6)), n(124, w ^ 0), n(123, a(2)), n(122, w ^ 0), n(121, op(+, 117, 120)), n(120, op(*, 118, 119)), n(119, a(4)), n(118, w ^ 0), n(117, a(0)), n(116, op(+, 100, 115)), n(115, op(*, 101, 114)), n(114, op(+, 106, 113)), n(113, op(*, 107, 112)), n(112, op(+, 108, 111)), n(111, op(*, 109, 110)), n(110, a(7)), n(109, w ^ 4), n(108, a(3)), n(107, w ^ 6), n(106, op(+, 102, 105)), n(105, op(*, 103, 104)), n(104, a(5)), n(103, w ^ 4), n(102, a(1)), n(101, w ^ 3), n(100, op(+, 92, 99)), n(99, op(*, 93, 98)), n(98, op(+, 94, 97)), n(97, op(*, 95, 96)), n(96, a(6)), n(95, w ^ 4), n(94, a(2)), n(93, w ^ 6), n(92, op(+, 88, 91)), n(91, op(*, 89, 90)), n(90, a(4)), n(89, w ^ 4), n(88, a(0)), n(87, op(+, 71, 86)), n(86, op(*, 72, 85)), n(85, op(+, 77, 84)), n(84, op(*, 78, 83)), n(83, op(+, 79, 82)), n(82, op(*, 80, 81)), n(81, a(7)), n(80, w ^ 0), n(79, a(3)), n(78, w ^ 4), n(77, op(+, 73, 76)), n(76, op(*, 74, 75)), n(75, a(5)), n(74, w ^ 0), n(73, a(1)), n(72, w ^ 2), n(71, op(+, 63, 70)), n(70, op(*, 64, 69)), n(69, op(+, 65, 68)), n(68, op(*, 66, 67)), n(67, a(6)), n(66, w ^ 0), n(65, a(2)), n(64, w ^ 4), n(63, op(+, 59, 62)), n(62, op(*, 60, 61)), n(61, a(4)), n(60, w ^ 0), n(59, a(0)), n(58, op(+, 42, 57)), n(57, op(*, 43, 56)), n(56, op(+, 48, 55)), n(55, op(*, 49, 54)), n(54, op(+, 50, 53)), n(53, op(*, 51, 52)), n(52, a(7)), n(51, w ^ 4), n(50, a(3)), n(49, w ^ 2), n(48, op(+, 44, 47)), n(47, op(*, 45, 46)), n(46, a(5)), n(45, w ^ 4), n(44, a(1)), n(43, w ^ 1), n(42, op(+, 34, 41)), n(41, op(*, 35, 40)), n(40, op(+, 36, 39)), n(39, op(*, 37, 38)), n(38, a(6)), n(37, w ^ 4), n(36, a(2)), n(35, w ^ 2), n(34, op(+, 30, 33)), n(33, op(*, 31, 32)), n(32, a(4)), n(31, w ^ 4), n(30, a(0)), n(29, op(+, 13, 28)), n(28, op(*, 14, 27)), n(27, op(+, 19, 26)), n(26, op(*, 20, 25)), n(25, op(+, 21, 24)), n(24, op(*, 22, 23)), n(23, a(7)), n(22, w ^ 0), n(21, a(3)), n(20, w ^ 0), n(19, op(+, 15, 18)), n(18, op(*, 16, 17)), n(17, a(5)), n(16, w ^ 0), n(15, a(1)), n(14, w ^ 0), n(13, op(+, 5, 12)), n(12, op(*, 6, 11)), n(11, op(+, 7, 10)), n(10, op(*, 8, 9)), n(9, a(6)), n(8, w ^ 0), n(7, a(2)), n(6, w ^ 0), n(5, op(+, 1, 4)), n(4, op(*, 2, 3)), n(3, a(4)), n(2, w ^ 0), n(1, a(0))]).
|
package/package.json
CHANGED
package/src/io.js
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
// Synchronous ISO stream state shared by a solver and all of its inner solvers.
|
|
2
2
|
import { BufferCtor, fs } from './platform.js';
|
|
3
3
|
|
|
4
|
+
const INVALID_UTF8_SENTINEL = '\udc00';
|
|
5
|
+
|
|
6
|
+
export class InvalidCharacterEncodingError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('invalid character encoding');
|
|
9
|
+
this.name = 'InvalidCharacterEncodingError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decodeUtf8ForTextStream(buffer) {
|
|
14
|
+
const bytes = buffer;
|
|
15
|
+
let text = '';
|
|
16
|
+
for (let i = 0; i < bytes.length;) {
|
|
17
|
+
const first = bytes[i];
|
|
18
|
+
if (first <= 0x7f) {
|
|
19
|
+
text += String.fromCodePoint(first);
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let width = 0;
|
|
25
|
+
let code = 0;
|
|
26
|
+
let minimum = 0;
|
|
27
|
+
if (first >= 0xc2 && first <= 0xdf) { width = 2; code = first & 0x1f; minimum = 0x80; }
|
|
28
|
+
else if (first >= 0xe0 && first <= 0xef) { width = 3; code = first & 0x0f; minimum = 0x800; }
|
|
29
|
+
else if (first >= 0xf0 && first <= 0xf4) { width = 4; code = first & 0x07; minimum = 0x10000; }
|
|
30
|
+
|
|
31
|
+
let valid = width !== 0 && i + width <= bytes.length;
|
|
32
|
+
if (valid) {
|
|
33
|
+
for (let j = 1; j < width; j++) {
|
|
34
|
+
const continuation = bytes[i + j];
|
|
35
|
+
if ((continuation & 0xc0) !== 0x80) { valid = false; break; }
|
|
36
|
+
code = (code << 6) | (continuation & 0x3f);
|
|
37
|
+
}
|
|
38
|
+
if (code < minimum || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) valid = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!valid) {
|
|
42
|
+
// A lone low surrogate cannot be produced by valid UTF-8. Keep it as a
|
|
43
|
+
// positional marker so open/4 succeeds and character input predicates
|
|
44
|
+
// can report representation_error(character) when the bad byte is read.
|
|
45
|
+
text += INVALID_UTF8_SENTINEL;
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
text += String.fromCodePoint(code);
|
|
50
|
+
i += width;
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
4
55
|
export class StreamManager {
|
|
5
56
|
constructor(options = {}) {
|
|
6
57
|
this.nextId = 2;
|
|
@@ -32,11 +83,17 @@ export class StreamManager {
|
|
|
32
83
|
if (!fs) throw new Error('file streams are unavailable in this runtime');
|
|
33
84
|
const type = options.type ?? 'text';
|
|
34
85
|
let content = type === 'binary' ? [] : '';
|
|
35
|
-
|
|
36
|
-
|
|
86
|
+
let strictUtf8 = false;
|
|
87
|
+
if (mode === 'read') {
|
|
88
|
+
const raw = fs.readFileSync(path);
|
|
89
|
+
if (type === 'binary') content = raw;
|
|
90
|
+
else { content = decodeUtf8ForTextStream(raw); strictUtf8 = true; }
|
|
91
|
+
} else if (mode === 'append' && fs.existsSync(path)) {
|
|
92
|
+
content = fs.readFileSync(path, type === 'binary' ? null : 'utf8');
|
|
93
|
+
}
|
|
37
94
|
if (BufferCtor?.isBuffer(content)) content = [...content];
|
|
38
95
|
return this.add({
|
|
39
|
-
id: this.nextId++, alias: options.alias ?? null, mode, type, content,
|
|
96
|
+
id: this.nextId++, alias: options.alias ?? null, mode, type, content, strictUtf8,
|
|
40
97
|
position: mode === 'append' ? content.length : 0, path,
|
|
41
98
|
reposition: options.reposition ?? false,
|
|
42
99
|
eofAction: options.eof_action ?? 'error', standard: false,
|
|
@@ -52,8 +109,19 @@ export class StreamManager {
|
|
|
52
109
|
}
|
|
53
110
|
readUnit(stream, peek = false) {
|
|
54
111
|
if (stream.position >= stream.content.length) return null;
|
|
55
|
-
|
|
56
|
-
|
|
112
|
+
if (stream.type === 'binary') {
|
|
113
|
+
const value = stream.content[stream.position];
|
|
114
|
+
if (!peek) stream.position++;
|
|
115
|
+
stream.pastEnd = false;
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
const source = String(stream.content);
|
|
119
|
+
if (stream.strictUtf8 && source[stream.position] === INVALID_UTF8_SENTINEL) {
|
|
120
|
+
throw new InvalidCharacterEncodingError();
|
|
121
|
+
}
|
|
122
|
+
const codePoint = source.codePointAt(stream.position);
|
|
123
|
+
const value = String.fromCodePoint(codePoint);
|
|
124
|
+
if (!peek) stream.position += value.length;
|
|
57
125
|
stream.pastEnd = false;
|
|
58
126
|
return value;
|
|
59
127
|
}
|
package/src/iso.js
CHANGED
|
@@ -172,6 +172,7 @@ export const isoBuiltins = {
|
|
|
172
172
|
export const eyePrologLibraryBuiltins = {
|
|
173
173
|
register(registry) {
|
|
174
174
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
175
|
+
registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
|
|
175
176
|
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
176
177
|
},
|
|
177
178
|
};
|
|
@@ -1023,7 +1024,15 @@ function inputUnitBuiltin(name) {
|
|
|
1023
1024
|
stream.pastEnd = false;
|
|
1024
1025
|
}
|
|
1025
1026
|
const peek = name.startsWith('peek');
|
|
1026
|
-
|
|
1027
|
+
let unit;
|
|
1028
|
+
try {
|
|
1029
|
+
unit = solver.io.readUnit(stream, peek);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (error?.name === 'InvalidCharacterEncodingError') {
|
|
1032
|
+
throw new PrologError('representation_error(character)');
|
|
1033
|
+
}
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1027
1036
|
if (unit == null && !peek) stream.pastEnd = true;
|
|
1028
1037
|
const result = unit == null ? (binary ? numberTerm(-1) : name.endsWith('code') ? numberTerm(-1) : atom('end_of_file'))
|
|
1029
1038
|
: binary ? numberTerm(unit) : name.endsWith('code') ? numberTerm(unit.codePointAt(0)) : atom(unit);
|
|
@@ -1072,7 +1081,7 @@ function isTerminatingFullStop(source, index) {
|
|
|
1072
1081
|
return true;
|
|
1073
1082
|
}
|
|
1074
1083
|
|
|
1075
|
-
const termGraphicCharacters = new Set('
|
|
1084
|
+
const termGraphicCharacters = new Set('#$&*+-./<=>?@^~\\:');
|
|
1076
1085
|
function continuesGraphicToken(source, index) {
|
|
1077
1086
|
return index > 0 && termGraphicCharacters.has(source[index - 1]);
|
|
1078
1087
|
}
|
|
@@ -1272,9 +1281,9 @@ function* readTermBuiltin({ solver, goal, env }) {
|
|
|
1272
1281
|
yield next;
|
|
1273
1282
|
}
|
|
1274
1283
|
function defaultTermWriteOptions(mode) {
|
|
1275
|
-
if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
|
|
1276
|
-
if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
|
|
1277
|
-
return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
|
|
1284
|
+
if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1285
|
+
if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1286
|
+
return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true, doubleQuotes: null };
|
|
1278
1287
|
}
|
|
1279
1288
|
|
|
1280
1289
|
function writeOptionBoolean(value, env, option) {
|
|
@@ -1323,6 +1332,7 @@ function termWriteOptions(term, env, mode = 'write') {
|
|
|
1323
1332
|
if (option.name === 'quoted') result.quoted = writeOptionBoolean(option.args[0], env, option);
|
|
1324
1333
|
else if (option.name === 'ignore_ops') result.ignoreOps = writeOptionBoolean(option.args[0], env, option);
|
|
1325
1334
|
else if (option.name === 'numbervars') result.numbervars = writeOptionBoolean(option.args[0], env, option);
|
|
1335
|
+
else if (option.name === 'double_quotes') result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
|
|
1326
1336
|
else if (option.name === 'variable_names') result.variableNames = writeVariableNames(option.args[0], env, option);
|
|
1327
1337
|
else throw new PrologError('domain_error(write_option)', option);
|
|
1328
1338
|
}
|
|
@@ -1345,6 +1355,11 @@ function* writeTermBuiltin({ solver, goal, env }) {
|
|
|
1345
1355
|
const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentOutput) : requireStream(solver, goal.args[0], env, 'write');
|
|
1346
1356
|
if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
|
|
1347
1357
|
const options = termWriteOptions(goal.args[goal.arity - 1], env);
|
|
1358
|
+
if (options.doubleQuotes === true) {
|
|
1359
|
+
options.doubleQuotes = solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars';
|
|
1360
|
+
} else {
|
|
1361
|
+
options.doubleQuotes = null;
|
|
1362
|
+
}
|
|
1348
1363
|
solver.io.writeUnit(stream, formatTermForWrite(goal.args[goal.arity - 2], env, {
|
|
1349
1364
|
...options,
|
|
1350
1365
|
operators: solver.program.operators.values(),
|
|
@@ -1667,6 +1682,8 @@ function numberListText(list, env, kind, valueIsBound) {
|
|
|
1667
1682
|
: String.fromCodePoint(Number(item.name))).join('');
|
|
1668
1683
|
}
|
|
1669
1684
|
|
|
1685
|
+
const numberSyntaxError = new PrologError('syntax_error(number)');
|
|
1686
|
+
|
|
1670
1687
|
function numberListBuiltin(kind) {
|
|
1671
1688
|
return function* ({ goal, env }) {
|
|
1672
1689
|
const value = deref(goal.args[0], env);
|
|
@@ -1678,7 +1695,7 @@ function numberListBuiltin(kind) {
|
|
|
1678
1695
|
if (value.type === NUMBER) {
|
|
1679
1696
|
if (text != null) {
|
|
1680
1697
|
const parsed = parseIsoNumber(text);
|
|
1681
|
-
if (parsed == null) throw
|
|
1698
|
+
if (parsed == null) throw numberSyntaxError;
|
|
1682
1699
|
if (sameNumber(value, parsed)) yield next;
|
|
1683
1700
|
return;
|
|
1684
1701
|
}
|
|
@@ -1688,7 +1705,7 @@ function numberListBuiltin(kind) {
|
|
|
1688
1705
|
return;
|
|
1689
1706
|
}
|
|
1690
1707
|
const parsed = parseIsoNumber(text);
|
|
1691
|
-
if (parsed == null) throw
|
|
1708
|
+
if (parsed == null) throw numberSyntaxError;
|
|
1692
1709
|
if (unify(goal.args[0], parsed, next)) yield next;
|
|
1693
1710
|
};
|
|
1694
1711
|
}
|
|
@@ -1835,6 +1852,32 @@ function* callClosureBuiltin({ solver, goal, env }) {
|
|
|
1835
1852
|
}
|
|
1836
1853
|
}
|
|
1837
1854
|
|
|
1855
|
+
function* countAllBuiltin({ solver, goal, env }) {
|
|
1856
|
+
const requested = deref(goal.args[1], env);
|
|
1857
|
+
// Validate Count before inspecting or executing Goal. This preserves the
|
|
1858
|
+
// expected error priority for e.g. countall(throw(x), -1).
|
|
1859
|
+
if (requested.type !== VAR) {
|
|
1860
|
+
if (requested.type !== NUMBER || !isDecimalInteger(requested.name)) {
|
|
1861
|
+
throw new PrologError('type_error(integer)', requested);
|
|
1862
|
+
}
|
|
1863
|
+
if (BigInt(requested.name) < 0n) {
|
|
1864
|
+
throw new PrologError('domain_error(not_less_than_zero)', requested);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const invoked = callable(goal.args[0], env);
|
|
1869
|
+
const child = solver.cloneForInnerGoal();
|
|
1870
|
+
let count = 0n;
|
|
1871
|
+
try {
|
|
1872
|
+
for (const _ of child.solve([invoked], env.clone(), 0)) count++;
|
|
1873
|
+
} finally {
|
|
1874
|
+
solver.absorbStatsFrom(child);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
const next = env.clone();
|
|
1878
|
+
if (unify(goal.args[1], numberTerm(count), next)) yield next;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1838
1881
|
function* callNthBuiltin({ solver, goal, env }) {
|
|
1839
1882
|
const requestedTerm = deref(goal.args[1], env);
|
|
1840
1883
|
// Zero is the one Nth value that fails before Goal is inspected.
|
package/src/lib/clpz.pl
CHANGED
|
@@ -8,21 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
:- module(clpz, [
|
|
11
|
-
'#>'/2,
|
|
12
|
-
'#<'/2,
|
|
13
|
-
'#>='/2,
|
|
14
|
-
'#=<'/2,
|
|
15
|
-
'#='/2,
|
|
16
|
-
'#\\='/2,
|
|
11
|
+
('#>')/2,
|
|
12
|
+
('#<')/2,
|
|
13
|
+
('#>=')/2,
|
|
14
|
+
('#=<')/2,
|
|
15
|
+
('#=')/2,
|
|
16
|
+
('#\\=')/2,
|
|
17
17
|
(#\\)/1,
|
|
18
|
-
'#<==>'/2,
|
|
19
|
-
'#==>'/2,
|
|
20
|
-
'#<=='/2,
|
|
21
|
-
'#\\/'/2,
|
|
18
|
+
('#<==>')/2,
|
|
19
|
+
('#==>')/2,
|
|
20
|
+
('#<==')/2,
|
|
21
|
+
('#\\/')/2,
|
|
22
22
|
(#\\)/2,
|
|
23
|
-
'#/\\'/2,
|
|
24
|
-
in/2,
|
|
25
|
-
ins/2,
|
|
23
|
+
('#/\\')/2,
|
|
24
|
+
('in')/2,
|
|
25
|
+
('ins')/2,
|
|
26
26
|
all_different/1,
|
|
27
27
|
all_distinct/1,
|
|
28
28
|
nvalue/2,
|
package/src/lib/iso_ext.pl
CHANGED
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
% Trealla. These definitions use only EyeProlog's ISO profile; extensions that
|
|
18
18
|
% require runtime cleanup, choice-point, alarm, or timeout hooks are omitted.
|
|
19
19
|
|
|
20
|
-
countall(Goal, Count) :-
|
|
21
|
-
findall(1, Goal, Ones),
|
|
22
|
-
iso_ext__length(Ones, 0, Count).
|
|
20
|
+
countall(Goal, Count) :- eyeprolog__countall(Goal, Count).
|
|
23
21
|
|
|
24
22
|
forall(Condition, Action) :-
|
|
25
23
|
findall(Action, Condition, Actions),
|
package/src/lib/lists.pl
CHANGED
|
@@ -66,7 +66,7 @@ max_list([X|Xs], Max) :- lists__max_list(Xs, X, Max).
|
|
|
66
66
|
|
|
67
67
|
list_to_set(List, Set) :- lists__list_to_set(List, [], Set).
|
|
68
68
|
|
|
69
|
-
countall(Goal, Count) :-
|
|
69
|
+
countall(Goal, Count) :- eyeprolog__countall(Goal, Count).
|
|
70
70
|
|
|
71
71
|
set_nth0(0, [_|Xs], X, [X|Xs]).
|
|
72
72
|
set_nth0(N, [Y|Ys], X, [Y|Zs]) :-
|
package/src/lib/prologue.pl
CHANGED
|
@@ -178,9 +178,7 @@ foldl(Closure, [A|As], [B|Bs], [C|Cs], Acc0, Acc) :-
|
|
|
178
178
|
call(Closure, A, B, C, Acc0, Acc1),
|
|
179
179
|
foldl(Closure, As, Bs, Cs, Acc1, Acc).
|
|
180
180
|
|
|
181
|
-
countall(Goal, Count) :-
|
|
182
|
-
findall(1, Goal, Ones),
|
|
183
|
-
length(Ones, Count).
|
|
181
|
+
countall(Goal, Count) :- eyeprolog__countall(Goal, Count).
|
|
184
182
|
|
|
185
183
|
prologue__integer_or_variable(X) :- var(X), !.
|
|
186
184
|
prologue__integer_or_variable(X) :- prologue__integer(X).
|
package/src/parser.js
CHANGED
|
@@ -33,7 +33,7 @@ function isPlainAtomStartCode(code) {
|
|
|
33
33
|
return code >= 97 && code <= 122;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const graphicAtomChars = '
|
|
36
|
+
const graphicAtomChars = '#$&*+-./<=>?@^~\\:';
|
|
37
37
|
|
|
38
38
|
// ISO operator syntax is lowered to the same ordinary compound terms used by
|
|
39
39
|
// canonical notation. Commas remain separators except inside parentheses.
|
|
@@ -650,7 +650,7 @@ class Parser {
|
|
|
650
650
|
const postfix = postfixName == null ? null : this.postfixOperators.get(postfixName);
|
|
651
651
|
if (!postfix || postfix.precedence < minPrecedence ||
|
|
652
652
|
(strictPostfixPrecedence === postfix.precedence)) break;
|
|
653
|
-
if (
|
|
653
|
+
if (leftIsBareOperatorAtom) {
|
|
654
654
|
throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
|
|
655
655
|
}
|
|
656
656
|
const name = postfixName;
|
|
@@ -659,7 +659,7 @@ class Parser {
|
|
|
659
659
|
strictPostfixPrecedence = postfix.strict ? postfix.precedence : null;
|
|
660
660
|
continue;
|
|
661
661
|
}
|
|
662
|
-
if (
|
|
662
|
+
if (leftIsBareOperatorAtom) {
|
|
663
663
|
throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
|
|
664
664
|
}
|
|
665
665
|
strictPostfixPrecedence = null;
|
|
@@ -682,7 +682,7 @@ class Parser {
|
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
|
-
if (
|
|
685
|
+
if (leftIsBareOperatorAtom && left.type === ATOM && !allowOperatorAtom) {
|
|
686
686
|
throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
|
|
687
687
|
}
|
|
688
688
|
return left;
|
|
@@ -693,7 +693,7 @@ class Parser {
|
|
|
693
693
|
// permits an operator atom directly as an `arg`; a leading `:-` cannot be
|
|
694
694
|
// prefix operator notation at argument priority, so it denotes the atom.
|
|
695
695
|
if (this.token.type === TOK.IF) {
|
|
696
|
-
if (
|
|
696
|
+
if (!allowOperatorAtom) {
|
|
697
697
|
throw new Error(`parse line ${this.token.line}: operator atom :- requires argument context or parentheses`);
|
|
698
698
|
}
|
|
699
699
|
this.advance();
|
|
@@ -732,7 +732,7 @@ class Parser {
|
|
|
732
732
|
// the operator as an atom instead of reporting a misleading bad-term
|
|
733
733
|
// error.
|
|
734
734
|
if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.BAR, TOK.DOT].includes(this.token.type)) {
|
|
735
|
-
if (
|
|
735
|
+
if (!allowOperatorAtom && this.token.type !== TOK.DOT) {
|
|
736
736
|
throw new Error(`parse line ${this.token.line}: operator atom ${op} requires argument context or parentheses`);
|
|
737
737
|
}
|
|
738
738
|
return atom(op);
|
|
@@ -788,11 +788,12 @@ class Parser {
|
|
|
788
788
|
}
|
|
789
789
|
if (this.token.type === TOK.ATOM) {
|
|
790
790
|
const name = this.token.text;
|
|
791
|
+
const quoted = this.token.quoted === true;
|
|
791
792
|
this.advance();
|
|
792
793
|
if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) {
|
|
793
794
|
return this.parseFunctionalNotation(name);
|
|
794
795
|
}
|
|
795
|
-
if (
|
|
796
|
+
if (!quoted && !allowOperatorAtom &&
|
|
796
797
|
(this.infixOperators.has(name) || this.prefixOperators.has(name) || this.postfixOperators.has(name))) {
|
|
797
798
|
throw new Error(`parse line ${this.token.line}: operator atom ${name} requires argument context or parentheses`);
|
|
798
799
|
}
|
|
@@ -1385,6 +1386,8 @@ export function parseProgramText(source, options = {}) {
|
|
|
1385
1386
|
return parseClauses(source, options);
|
|
1386
1387
|
}
|
|
1387
1388
|
|
|
1389
|
+
const invalidNumberTokenError = new Error('not exactly one number token');
|
|
1390
|
+
|
|
1388
1391
|
export function parseNumberTokenText(text) {
|
|
1389
1392
|
const source = String(text ?? '');
|
|
1390
1393
|
let position = 0;
|
|
@@ -1401,20 +1404,20 @@ export function parseNumberTokenText(text) {
|
|
|
1401
1404
|
position += 2;
|
|
1402
1405
|
let value = source[position] ?? '';
|
|
1403
1406
|
if (!value || (value !== ' ' && isWhitespaceCode(value.charCodeAt(0)))) {
|
|
1404
|
-
throw
|
|
1407
|
+
throw invalidNumberTokenError;
|
|
1405
1408
|
}
|
|
1406
1409
|
position++;
|
|
1407
1410
|
const firstCode = value.charCodeAt(0);
|
|
1408
1411
|
if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
|
|
1409
1412
|
const secondCode = source.charCodeAt(position);
|
|
1410
|
-
if (secondCode < 0xdc00 || secondCode > 0xdfff) throw
|
|
1413
|
+
if (secondCode < 0xdc00 || secondCode > 0xdfff) throw invalidNumberTokenError;
|
|
1411
1414
|
value += source[position++];
|
|
1412
1415
|
} else if (firstCode >= 0xdc00 && firstCode <= 0xdfff) {
|
|
1413
|
-
throw
|
|
1416
|
+
throw invalidNumberTokenError;
|
|
1414
1417
|
}
|
|
1415
1418
|
|
|
1416
1419
|
if (value === "'") {
|
|
1417
|
-
if (source[position] !== "'") throw
|
|
1420
|
+
if (source[position] !== "'") throw invalidNumberTokenError;
|
|
1418
1421
|
position++;
|
|
1419
1422
|
} else if (value === '\\') {
|
|
1420
1423
|
const escaped = source[position++] ?? '';
|
|
@@ -1424,25 +1427,25 @@ export function parseNumberTokenText(text) {
|
|
|
1424
1427
|
} else if (escaped === 'x') {
|
|
1425
1428
|
let digits = '';
|
|
1426
1429
|
while (/^[0-9A-Fa-f]$/.test(source[position] ?? '')) digits += source[position++];
|
|
1427
|
-
if (!digits || source[position++] !== '\\') throw
|
|
1430
|
+
if (!digits || source[position++] !== '\\') throw invalidNumberTokenError;
|
|
1428
1431
|
const code = Number.parseInt(digits, 16);
|
|
1429
|
-
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw
|
|
1432
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
|
|
1430
1433
|
value = String.fromCodePoint(code);
|
|
1431
1434
|
} else if (/^[0-7]$/.test(escaped)) {
|
|
1432
1435
|
let digits = escaped;
|
|
1433
1436
|
while (/^[0-7]$/.test(source[position] ?? '')) digits += source[position++];
|
|
1434
|
-
if (source[position++] !== '\\') throw
|
|
1437
|
+
if (source[position++] !== '\\') throw invalidNumberTokenError;
|
|
1435
1438
|
const code = Number.parseInt(digits, 8);
|
|
1436
|
-
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw
|
|
1439
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
|
|
1437
1440
|
value = String.fromCodePoint(code);
|
|
1438
1441
|
} else if (escaped === '\\' || escaped === "'" || escaped === '"' || escaped === '`') {
|
|
1439
1442
|
value = escaped;
|
|
1440
1443
|
} else {
|
|
1441
|
-
throw
|
|
1444
|
+
throw invalidNumberTokenError;
|
|
1442
1445
|
}
|
|
1443
1446
|
}
|
|
1444
1447
|
|
|
1445
|
-
if (position !== source.length) throw
|
|
1448
|
+
if (position !== source.length) throw invalidNumberTokenError;
|
|
1446
1449
|
const code = value.codePointAt(0);
|
|
1447
1450
|
return numberTerm(String(negative ? -code : code));
|
|
1448
1451
|
}
|
|
@@ -1454,7 +1457,7 @@ export function parseNumberTokenText(text) {
|
|
|
1454
1457
|
position += 2;
|
|
1455
1458
|
let digits = '';
|
|
1456
1459
|
while (digitPattern.test(source[position] ?? '')) digits += source[position++];
|
|
1457
|
-
if (!digits || position !== source.length) throw
|
|
1460
|
+
if (!digits || position !== source.length) throw invalidNumberTokenError;
|
|
1458
1461
|
let integer = 0n;
|
|
1459
1462
|
for (const digit of digits) integer = integer * BigInt(radix) + BigInt(Number.parseInt(digit, radix));
|
|
1460
1463
|
if (negative) integer = -integer;
|
|
@@ -1463,7 +1466,7 @@ export function parseNumberTokenText(text) {
|
|
|
1463
1466
|
|
|
1464
1467
|
const digitsStart = position;
|
|
1465
1468
|
while (isDigitCode(source.charCodeAt(position))) position++;
|
|
1466
|
-
if (position === digitsStart) throw
|
|
1469
|
+
if (position === digitsStart) throw invalidNumberTokenError;
|
|
1467
1470
|
let hasFraction = false;
|
|
1468
1471
|
if (source[position] === '.' && isDigitCode(source.charCodeAt(position + 1))) {
|
|
1469
1472
|
hasFraction = true;
|
|
@@ -1475,9 +1478,9 @@ export function parseNumberTokenText(text) {
|
|
|
1475
1478
|
if (source[position] === '+' || source[position] === '-') position++;
|
|
1476
1479
|
const exponentStart = position;
|
|
1477
1480
|
while (isDigitCode(source.charCodeAt(position))) position++;
|
|
1478
|
-
if (position === exponentStart) throw
|
|
1481
|
+
if (position === exponentStart) throw invalidNumberTokenError;
|
|
1479
1482
|
}
|
|
1480
|
-
if (position !== source.length) throw
|
|
1483
|
+
if (position !== source.length) throw invalidNumberTokenError;
|
|
1481
1484
|
if (/^-?\d+$/.test(source)) return numberTerm(BigInt(source).toString());
|
|
1482
1485
|
if (Object.is(Number(source), -0)) return numberTerm('0.0');
|
|
1483
1486
|
return numberTerm(source);
|
package/src/repl.js
CHANGED
|
@@ -614,6 +614,7 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
614
614
|
// current operator atoms in argument and list-element positions without
|
|
615
615
|
// quotes, just as writeq/1 already prints them.
|
|
616
616
|
operatorAtomsAsArgs: true,
|
|
617
|
+
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
617
618
|
})}`);
|
|
618
619
|
}
|
|
619
620
|
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
package/src/solver.js
CHANGED
|
@@ -291,8 +291,8 @@ export class Solver {
|
|
|
291
291
|
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
292
292
|
if (goal.type === 'atom' && goal.name === '!' && goal.arity === 0) {
|
|
293
293
|
const marker = active[active.length - 1] ?? null;
|
|
294
|
-
this.cutEpoch++;
|
|
295
294
|
if (marker) marker.cutEpoch = (marker.cutEpoch ?? 0) + 1;
|
|
295
|
+
else this.cutEpoch++;
|
|
296
296
|
for (const solveStack of this.solveStacks) {
|
|
297
297
|
for (let i = solveStack.length - 1; i >= 0; i--) {
|
|
298
298
|
if (marker == null || solveStack[i].active?.includes(marker)) solveStack.splice(i, 1);
|
|
@@ -446,6 +446,14 @@ export class Solver {
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
checkMemoryReservation(bytes) {
|
|
450
|
+
if (!Number.isFinite(this.maxMemoryBytes) || !Number.isFinite(bytes) || bytes <= 0) return;
|
|
451
|
+
const used = usedHeapSize();
|
|
452
|
+
if (used != null && bytes > Math.max(0, this.maxMemoryBytes - used)) {
|
|
453
|
+
throw new PrologError('resource_error(memory)');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
449
457
|
*solveUserGoal(goal, rest, env, depth) {
|
|
450
458
|
this.stats.solve_one_goal_calls++;
|
|
451
459
|
if (depth > this.maxDepth) {
|
|
@@ -764,6 +772,14 @@ function* fixedLengthSolutions(solver, list, length, env) {
|
|
|
764
772
|
return;
|
|
765
773
|
}
|
|
766
774
|
|
|
775
|
+
// A materialized list cell with its fresh variable occupies roughly 216
|
|
776
|
+
// bytes on current V8 builds. Reserve conservatively before constructing a
|
|
777
|
+
// huge list so a resource_error does not leave the heap at the soft limit
|
|
778
|
+
// and poison the next query.
|
|
779
|
+
const estimatedListCellBytes = 256n;
|
|
780
|
+
if (remaining > BigInt(Number.MAX_SAFE_INTEGER)) throw new PrologError('resource_error(memory)');
|
|
781
|
+
solver.checkMemoryReservation(Number(remaining * estimatedListCellBytes));
|
|
782
|
+
|
|
767
783
|
const id = nextFreshId();
|
|
768
784
|
let suffix = emptyList();
|
|
769
785
|
for (let index = 0n; index < remaining; index++) {
|
package/src/standard-library.js
CHANGED
|
@@ -45,7 +45,7 @@ function libraryUrl(filename) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
48
|
-
'call_nth/2', 'freeze/2',
|
|
48
|
+
'call_nth/2', 'freeze/2', 'countall/2',
|
|
49
49
|
'#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
|
|
50
50
|
'#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
|
|
51
51
|
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
|
@@ -65,7 +65,7 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
|
65
65
|
'matches/2', 'join/3', 'substring/4', 'member/2', 'select/3', 'last/2',
|
|
66
66
|
'nth0/3', 'nth0/4', 'nth1/3', 'nth1/4', 'set_nth0/4', 'take/3', 'drop/3', 'slice/4', 'reverse/2',
|
|
67
67
|
'length/2', 'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
|
|
68
|
-
'
|
|
68
|
+
'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
|
|
69
69
|
'forall/2', 'cfor/3', 'findall/4', 'variant/2', 'uuid/3',
|
|
70
70
|
]);
|
|
71
71
|
export const eyePrologLibraryIndicators = Object.freeze([
|
package/src/write.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
Env, deref, isCons, isEmptyList,
|
|
5
5
|
} from './term.js';
|
|
6
6
|
|
|
7
|
-
const graphicAtomCharacters = new Set('
|
|
7
|
+
const graphicAtomCharacters = new Set('!#$&*+-/<=>?@^~\\'.split(''));
|
|
8
8
|
const compactInfixOperators = new Set([':', '..']);
|
|
9
9
|
|
|
10
10
|
function quotedControlEscape(ch) {
|
|
@@ -29,7 +29,6 @@ function atomNeedsQuotes(name) {
|
|
|
29
29
|
if (name === '[]' || name === '{}') return false;
|
|
30
30
|
if (name === '...') return false;
|
|
31
31
|
if (name.startsWith('/*')) return true;
|
|
32
|
-
if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
|
|
33
32
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
|
|
34
33
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
|
35
34
|
return false;
|
|
@@ -107,7 +106,7 @@ function writeNumberedVariable(index) {
|
|
|
107
106
|
function operatorName(name) {
|
|
108
107
|
if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
|
|
109
108
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return name;
|
|
110
|
-
if (/^[
|
|
109
|
+
if (/^[!#$&*+\-./<=>?@^~\\;:]+$/.test(name)) return name;
|
|
111
110
|
return quoteAtom(name);
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -178,6 +177,10 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
178
177
|
// without quoting. Keep lexical exceptions such as `|` quoted.
|
|
179
178
|
if (options.operatorAtomsAsArgs && context === 'argument' && table.has(resolved.name)) return operatorName(resolved.name);
|
|
180
179
|
if (!options.ignoreOps && context !== 'argument' && table.has(resolved.name)) {
|
|
180
|
+
// The predefined ?- atom is safe at the end of a written term and is a
|
|
181
|
+
// graphic atom in ISO syntax. Do not add the legacy parentheses that
|
|
182
|
+
// issue #35 reports for writeq(?-).
|
|
183
|
+
if (resolved.name === '?-') return operatorName(resolved.name);
|
|
181
184
|
const definitions = table.get(resolved.name);
|
|
182
185
|
const requiresParentheses = definitions.some(({ specifier }) =>
|
|
183
186
|
['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(specifier));
|
|
@@ -35,7 +35,7 @@ error-ordering alternative to an individual executable assertion.
|
|
|
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. |
|
|
37
37
|
| 7.7 — execution and backtracking | audit | Control/search suites. Strict mode disables EyeProlog automatic tabling, cycle guards, and recursive numeric shortcuts so core execution uses ordinary clause selection/backtracking. |
|
|
38
|
-
| 7.8 — control constructs and exceptions | audit | call, cut, conjunction, disjunction, if-then, catch/throw, renamed-copy tests. |
|
|
38
|
+
| 7.8 — control constructs and exceptions | audit | call, cut, conjunction, disjunction (including failed branches after callee-local cuts), if-then, catch/throw, renamed-copy tests. |
|
|
39
39
|
| 7.9 — expression evaluation | audit | Arithmetic/evaluation/error suites, including Corrigenda. Exceptional-value/error-precedence matrix remains to be exhaustively enumerated. |
|
|
40
40
|
| 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, Corrigendum 3 writer cases. Full option cross-product remains open. |
|
|
41
41
|
| 7.11 — flags | audit | Required Part 1 flags implemented. Normal and strict modes use the ISO `unknown=error` default; strict mode excludes the EyeProlog `occurs_check` extension. |
|
|
@@ -9,7 +9,8 @@ The suite is intentionally file-based. Exact standard output, errors, warnings,
|
|
|
9
9
|
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
|
-
executable cases.
|
|
12
|
+
executable cases. [WG17-SYNTAX-STATUS.md](WG17-SYNTAX-STATUS.md) records the
|
|
13
|
+
complete one-to-one trace for all 366 active upstream syntax cases.
|
|
13
14
|
|
|
14
15
|
“Conformance” here means conformance to EyeProlog's documented ISO compatibility
|
|
15
16
|
profile and implementation extensions. The default registry covers the exact
|
|
@@ -68,6 +69,12 @@ Run the Part 1 + Corrigenda strict-core processor gate:
|
|
|
68
69
|
npm run test:iso-strict
|
|
69
70
|
```
|
|
70
71
|
|
|
72
|
+
Run the complete WG17 syntax matrix independently:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npm run test:wg17-syntax
|
|
76
|
+
```
|
|
77
|
+
|
|
71
78
|
Summarize conformance coverage by category:
|
|
72
79
|
|
|
73
80
|
```sh
|
|
@@ -102,10 +109,11 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
102
109
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
103
110
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
104
111
|
|
|
105
|
-
The corpus has 386 cases in `iso/` and
|
|
106
|
-
total. The
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
The corpus has 386 cases in `iso/` and 791 file-based conformance cases in
|
|
113
|
+
total. The separate strict-reader WG17 matrix has 366 executable dispositions.
|
|
114
|
+
The generated `conformance-report.md` is the authoritative source for current
|
|
115
|
+
category totals. Together with regression, documentation-sync, API, example,
|
|
116
|
+
and book-example checks, `npm test` is the release gate.
|
|
109
117
|
|
|
110
118
|
## Updating expected output
|
|
111
119
|
|
|
@@ -33,6 +33,16 @@ if_then_cut(Value) :-
|
|
|
33
33
|
(true -> (!, choice(Value)); fail).
|
|
34
34
|
if_then_cut(fallback).
|
|
35
35
|
|
|
36
|
+
callee_cut_then_fail :-
|
|
37
|
+
!,
|
|
38
|
+
fail.
|
|
39
|
+
|
|
40
|
+
callee_cut_disjunction :-
|
|
41
|
+
(callee_cut_then_fail; true).
|
|
42
|
+
|
|
43
|
+
between_disjunction :-
|
|
44
|
+
(between(1, 1, _), false; true).
|
|
45
|
+
|
|
36
46
|
%% goal: committed_answer(X0)
|
|
37
47
|
|
|
38
48
|
committed_answer(Value) :-
|
|
@@ -67,3 +77,13 @@ call_local_answer(Kind, Value) :-
|
|
|
67
77
|
|
|
68
78
|
if_then_answer(Value) :-
|
|
69
79
|
if_then_cut(Value).
|
|
80
|
+
|
|
81
|
+
%% goal: callee_cut_disjunction_answer
|
|
82
|
+
|
|
83
|
+
callee_cut_disjunction_answer :-
|
|
84
|
+
callee_cut_disjunction.
|
|
85
|
+
|
|
86
|
+
%% goal: between_disjunction_answer
|
|
87
|
+
|
|
88
|
+
between_disjunction_answer :-
|
|
89
|
+
between_disjunction.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
question_mark_graphics(?, ??).
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
operator_atoms(1200, xfx, [':-',
|
|
2
|
-
operator_atoms(1200, fx, [':-',
|
|
1
|
+
operator_atoms(1200, xfx, [':-', -]).
|
|
2
|
+
operator_atoms(1200, fx, [':-', -]).
|
|
3
3
|
query_prefix_operator(ok).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ), got x
|
package/test/conformance/expected-errors/variables/bare_underscore_name_in_compound_rejected.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ), got value
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ], got x
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ), got 1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ), got _name
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ), got x
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ), got x
|
|
@@ -1272,7 +1272,7 @@
|
|
|
1272
1272
|
"variables": "[]"
|
|
1273
1273
|
},
|
|
1274
1274
|
{
|
|
1275
|
-
"output": "
|
|
1275
|
+
"output": "-(1)",
|
|
1276
1276
|
"variables": "[]"
|
|
1277
1277
|
}
|
|
1278
1278
|
]
|
|
@@ -4069,7 +4069,7 @@
|
|
|
4069
4069
|
"variables": "[]"
|
|
4070
4070
|
},
|
|
4071
4071
|
{
|
|
4072
|
-
"output": "$(
|
|
4072
|
+
"output": "$(+(a,b))",
|
|
4073
4073
|
"variables": "[]"
|
|
4074
4074
|
}
|
|
4075
4075
|
]
|
|
@@ -4128,7 +4128,7 @@
|
|
|
4128
4128
|
"type": "success",
|
|
4129
4129
|
"stages": [
|
|
4130
4130
|
{
|
|
4131
|
-
"output": "
|
|
4131
|
+
"output": "+(__anon0,__anon1)",
|
|
4132
4132
|
"variables": "[]"
|
|
4133
4133
|
}
|
|
4134
4134
|
]
|
|
@@ -4144,7 +4144,7 @@
|
|
|
4144
4144
|
"type": "success",
|
|
4145
4145
|
"stages": [
|
|
4146
4146
|
{
|
|
4147
|
-
"output": "
|
|
4147
|
+
"output": "+(B,B)",
|
|
4148
4148
|
"variables": "['B' = B]"
|
|
4149
4149
|
}
|
|
4150
4150
|
]
|
|
@@ -4246,7 +4246,7 @@
|
|
|
4246
4246
|
"type": "success",
|
|
4247
4247
|
"stages": [
|
|
4248
4248
|
{
|
|
4249
|
-
"output": "
|
|
4249
|
+
"output": "-(a,-(-(b)))",
|
|
4250
4250
|
"variables": "[]"
|
|
4251
4251
|
}
|
|
4252
4252
|
]
|
package/test/run-regression.mjs
CHANGED
|
@@ -659,7 +659,7 @@ c4 ?- call((!;1)).
|
|
|
659
659
|
input: 'X = -0.0, number_chars(X,C), number_chars(Y,C), X == Y, number_chars(Y,D).\nhalt.\n',
|
|
660
660
|
});
|
|
661
661
|
assertEqual(result.status, 0, 'exit status');
|
|
662
|
-
assertIncludes(result.stdout,
|
|
662
|
+
assertIncludes(result.stdout, 'X = 0.0, C = "0.0", Y = 0.0, D = "0.0".', 'answer');
|
|
663
663
|
assertEqual(result.stderr, '', 'stderr');
|
|
664
664
|
},
|
|
665
665
|
},
|
|
@@ -672,6 +672,28 @@ c4 ?- call((!;1)).
|
|
|
672
672
|
});
|
|
673
673
|
assertEqual(streamResult.stdout, 'read(46 = 46).\n', 'read/1 character-code full stop');
|
|
674
674
|
|
|
675
|
+
// A complete term must be recognized from its full stop alone. Do not
|
|
676
|
+
// consult the interactive refill hook (which would amount to waiting
|
|
677
|
+
// for EOF or another line) once the terminator has been seen.
|
|
678
|
+
const program = Program.parse('');
|
|
679
|
+
const solver = new Solver(program, {
|
|
680
|
+
registry: getEyePrologRegistry(),
|
|
681
|
+
ioOptions: { input: "X = 0'. .\n" },
|
|
682
|
+
});
|
|
683
|
+
const inputStream = solver.io.resolve('user_input');
|
|
684
|
+
let refillRequests = 0;
|
|
685
|
+
inputStream.interactiveReadTerm = () => {
|
|
686
|
+
refillRequests++;
|
|
687
|
+
throw new Error('reader requested input after a complete term');
|
|
688
|
+
};
|
|
689
|
+
const readGoal = parseGoalText('read(T)', { operatorDefinitions: [...program.operators.values()] });
|
|
690
|
+
const readAnswers = [...solver.solve([readGoal], new Env(), 0)];
|
|
691
|
+
assertEqual(readAnswers.length, 1, 'read/1 answer without EOF');
|
|
692
|
+
assertEqual(refillRequests, 0, 'read/1 terminates without EOF assistance');
|
|
693
|
+
const readTerm = copyResolved(readGoal.args[0], readAnswers[0]);
|
|
694
|
+
assertEqual(readTerm.name, '=', 'read/1 parsed assignment');
|
|
695
|
+
assertEqual(readTerm.args[1].name, '46', 'read/1 parsed character code');
|
|
696
|
+
|
|
675
697
|
const cliResult = runCli([], { input: "X = 0'. .\nhalt.\n" });
|
|
676
698
|
assertEqual(cliResult.status, 0, 'top-level character-code full stop status');
|
|
677
699
|
assertIncludes(cliResult.stdout, 'X = 46', 'top-level character-code answer');
|
|
@@ -682,6 +704,30 @@ c4 ?- call((!;1)).
|
|
|
682
704
|
assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
|
|
683
705
|
},
|
|
684
706
|
},
|
|
707
|
+
{
|
|
708
|
+
name: 'question mark is a graphic character and writeq keeps graphic atoms unquoted',
|
|
709
|
+
run: () => {
|
|
710
|
+
const result = runCli([], { input: 'writeq(?).\nwriteq(??).\nwriteq(?-).\nhalt.\n' });
|
|
711
|
+
assertEqual(result.status, 0, 'exit status');
|
|
712
|
+
assertIncludes(result.stdout, '? true.', 'writeq(?)');
|
|
713
|
+
assertIncludes(result.stdout, '?? true.', 'writeq(??)');
|
|
714
|
+
assertIncludes(result.stdout, '?- true.', 'writeq(?-)');
|
|
715
|
+
assertNotIncludes(result.stdout, "'?-'", 'writeq(?-) has no quotes');
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: 'normal parser rejects non-conforming bare operator operands',
|
|
720
|
+
run: () => {
|
|
721
|
+
let caught = null;
|
|
722
|
+
try {
|
|
723
|
+
parseGoalText('write_canonical((- = - 1))');
|
|
724
|
+
} catch (error) {
|
|
725
|
+
caught = error;
|
|
726
|
+
}
|
|
727
|
+
if (!caught) throw new Error('non-conforming operator operand unexpectedly parsed');
|
|
728
|
+
assertIncludes(caught.message, 'operator atom', 'syntax rejection');
|
|
729
|
+
},
|
|
730
|
+
},
|
|
685
731
|
{
|
|
686
732
|
name: 'layout distinguishes prefix notation from functional notation',
|
|
687
733
|
run: () => {
|
|
@@ -1820,17 +1866,17 @@ c4 ?- call((!;1)).
|
|
|
1820
1866
|
run: () => {
|
|
1821
1867
|
assertEqual(
|
|
1822
1868
|
run('', { goal: 'current_op(Priority, Specifier, ?-)' }).stdout,
|
|
1823
|
-
"current_op(1200, fx,
|
|
1869
|
+
"current_op(1200, fx, ?-).\ncurrent_op(1200, xfx, ?-).\n",
|
|
1824
1870
|
'query operator definitions',
|
|
1825
1871
|
);
|
|
1826
1872
|
assertEqual(
|
|
1827
1873
|
run('', { goal: 'current_op(1200, fx, ?-)' }).stdout,
|
|
1828
|
-
"current_op(1200, fx,
|
|
1874
|
+
"current_op(1200, fx, ?-).\n",
|
|
1829
1875
|
'ISO query prefix operator',
|
|
1830
1876
|
);
|
|
1831
1877
|
assertEqual(
|
|
1832
1878
|
run('', { goal: 'current_op(1200, xfx, ?-)' }).stdout,
|
|
1833
|
-
"current_op(1200, xfx,
|
|
1879
|
+
"current_op(1200, xfx, ?-).\n",
|
|
1834
1880
|
'quad query infix operator',
|
|
1835
1881
|
);
|
|
1836
1882
|
const prefix = parseGoalText('(?- true)');
|
|
@@ -2035,6 +2081,23 @@ c4 ?- call((!;1)).
|
|
|
2035
2081
|
assertEqual(run(source, { goal: 'read_codes(ok)' }).stdout, 'read_codes(ok).\n', 'double_quotes read flag');
|
|
2036
2082
|
},
|
|
2037
2083
|
},
|
|
2084
|
+
{
|
|
2085
|
+
name: 'get_char and peek_char report invalid UTF-8 as representation_error(character)',
|
|
2086
|
+
run: () => {
|
|
2087
|
+
const invalidPath = path.join(tmp, `invalid-utf8-${++tmpCounter}.bin`);
|
|
2088
|
+
fs.writeFileSync(invalidPath, Buffer.from([0xff]));
|
|
2089
|
+
const quotedPath = sourceAtom(invalidPath);
|
|
2090
|
+
for (const predicate of ['peek_char', 'get_char']) {
|
|
2091
|
+
let caught = null;
|
|
2092
|
+
try {
|
|
2093
|
+
run('', { goal: `open(${quotedPath}, read, S, []), ${predicate}(S, C)` });
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
caught = error;
|
|
2096
|
+
}
|
|
2097
|
+
assertEqual(caught?.formal, 'representation_error(character)', `${predicate}/2 invalid UTF-8`);
|
|
2098
|
+
}
|
|
2099
|
+
},
|
|
2100
|
+
},
|
|
2038
2101
|
{
|
|
2039
2102
|
name: 'write predicates and write_term options select distinct formats',
|
|
2040
2103
|
run: () => {
|
|
@@ -2050,16 +2113,26 @@ c4 ?- call((!;1)).
|
|
|
2050
2113
|
" write_term(a+b, [ignore_ops(false)]), put_char('|'),",
|
|
2051
2114
|
" write_term('$VAR'(0), [numbervars(true)]), put_char('|'),",
|
|
2052
2115
|
" write_term('$VAR'(0), [numbervars(false)]), put_char('|'),",
|
|
2053
|
-
" write_term(pair(X, Y), [variable_names(['Left'=X, 'Right'=Y])])
|
|
2116
|
+
" write_term(pair(X, Y), [variable_names(['Left'=X, 'Right'=Y])]), put_char('|'),",
|
|
2117
|
+
` write_term("ab", [double_quotes(true)]), put_char('|'),`,
|
|
2118
|
+
' write_term("ab", [double_quotes(false)]).',
|
|
2054
2119
|
'',
|
|
2055
2120
|
].join('\n');
|
|
2056
2121
|
assertEqual(
|
|
2057
2122
|
run(source, { goal: 'emit' }).stdout,
|
|
2058
|
-
"hello world|'hello world'|a + b * c
|
|
2123
|
+
"hello world|'hello world'|a + b * c|+(a,*(b,c))|hello world|'hello world'|+(a,b)|a + b|A|$VAR(0)|pair(Left,Right)|\"ab\"|[a,b]emit.\n",
|
|
2059
2124
|
'stdout',
|
|
2060
2125
|
);
|
|
2061
2126
|
},
|
|
2062
2127
|
},
|
|
2128
|
+
{
|
|
2129
|
+
name: 'REPL renders character lists with double quotes',
|
|
2130
|
+
run: () => {
|
|
2131
|
+
const result = runCli([], { input: 'L="UN-READABLE, ...".\nhalt.\n' });
|
|
2132
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2133
|
+
assertIncludes(result.stdout, 'L = "UN-READABLE, ...".', 'top-level character-list rendering');
|
|
2134
|
+
},
|
|
2135
|
+
},
|
|
2063
2136
|
{
|
|
2064
2137
|
name: 'CLI false/0 fails as an ordinary goal',
|
|
2065
2138
|
run: () => {
|
|
@@ -2593,11 +2666,11 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2593
2666
|
const programText = `
|
|
2594
2667
|
:- use_module(library(lists)).
|
|
2595
2668
|
alphabet(['0','1','2','3','4','5','6','7','8','9','.']).
|
|
2596
|
-
trial([A,B,C,D,E]) :-
|
|
2669
|
+
trial([A,B,C,D,E,F]) :-
|
|
2597
2670
|
alphabet(Chars),
|
|
2598
2671
|
member(A, Chars), member(B, Chars), member(C, Chars),
|
|
2599
|
-
member(D, Chars), member(E, Chars),
|
|
2600
|
-
catch(number_chars(_, [A,B,C,D,E]), error(syntax_error(number), _), true).
|
|
2672
|
+
member(D, Chars), member(E, Chars), member(F, Chars),
|
|
2673
|
+
catch(number_chars(_, [A,B,C,D,E,F]), error(syntax_error(number), _), true).
|
|
2601
2674
|
`;
|
|
2602
2675
|
const script = `
|
|
2603
2676
|
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
@@ -2606,9 +2679,9 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2606
2679
|
const goal = parseGoalText('trial(Chars)');
|
|
2607
2680
|
let count = 0;
|
|
2608
2681
|
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2609
|
-
if (++count ===
|
|
2682
|
+
if (++count === 500000) break;
|
|
2610
2683
|
}
|
|
2611
|
-
if (count !==
|
|
2684
|
+
if (count !== 500000) throw new Error('unexpected answer count: ' + count);
|
|
2612
2685
|
process.stdout.write(String(count));
|
|
2613
2686
|
`;
|
|
2614
2687
|
const result = spawnSync(process.execPath, [
|
|
@@ -2616,10 +2689,10 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2616
2689
|
'--input-type=module',
|
|
2617
2690
|
'--eval',
|
|
2618
2691
|
script,
|
|
2619
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout:
|
|
2692
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 45000 });
|
|
2620
2693
|
if (result.error) throw result.error;
|
|
2621
2694
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2622
|
-
assertEqual(result.stdout, '
|
|
2695
|
+
assertEqual(result.stdout, '500000', 'distinct number syntax attempts');
|
|
2623
2696
|
},
|
|
2624
2697
|
},
|
|
2625
2698
|
{
|
|
@@ -2647,22 +2720,23 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2647
2720
|
},
|
|
2648
2721
|
},
|
|
2649
2722
|
{
|
|
2650
|
-
name: '
|
|
2723
|
+
name: 'resource_error(memory) leaves enough heap for subsequent list work',
|
|
2651
2724
|
run: () => {
|
|
2652
2725
|
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2653
|
-
const programText = ':- use_module(library(prologue)).\n';
|
|
2654
|
-
const goalText = '\\+ \\+ length(List, 1000000)';
|
|
2655
2726
|
const script = `
|
|
2656
2727
|
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2657
|
-
const program = Program.parse(${JSON.stringify(
|
|
2728
|
+
const program = Program.parse(${JSON.stringify(':- use_module(library(prologue)).\n')}, { sourceMetadata: false });
|
|
2658
2729
|
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2659
|
-
const
|
|
2660
|
-
operatorDefinitions: [...program.operators.values()]
|
|
2661
|
-
|
|
2730
|
+
const execute = (text) => {
|
|
2731
|
+
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
2732
|
+
return [...solver.solve([goal], new Env(), 0)];
|
|
2733
|
+
};
|
|
2734
|
+
execute('length(L,1000),fail');
|
|
2662
2735
|
let caught = null;
|
|
2663
|
-
try {
|
|
2736
|
+
try { execute('length(L,1000000),fail'); } catch (error) { caught = error; }
|
|
2664
2737
|
if (caught?.formal !== 'resource_error(memory)') throw caught ?? new Error('no resource error');
|
|
2665
|
-
|
|
2738
|
+
execute('length(L,1000),fail');
|
|
2739
|
+
process.stdout.write('recovered');
|
|
2666
2740
|
`;
|
|
2667
2741
|
const result = spawnSync(process.execPath, [
|
|
2668
2742
|
'--max-old-space-size=64',
|
|
@@ -2672,7 +2746,7 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2672
2746
|
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2673
2747
|
if (result.error) throw result.error;
|
|
2674
2748
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2675
|
-
assertEqual(result.stdout, '
|
|
2749
|
+
assertEqual(result.stdout, 'recovered', 'solver remains usable after resource error');
|
|
2676
2750
|
},
|
|
2677
2751
|
},
|
|
2678
2752
|
{
|
|
@@ -2739,12 +2813,12 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2739
2813
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
2740
2814
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
2741
2815
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
2742
|
-
assertEqual(library.defs.size,
|
|
2816
|
+
assertEqual(library.defs.size, 153, 'EyeProlog registry contains ISO definitions and private library adapters');
|
|
2743
2817
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
2744
2818
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
2745
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
2746
|
-
assertEqual(eyePrologPortableLibraryIndicators.length,
|
|
2747
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
2819
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
|
|
2820
|
+
assertEqual(eyePrologPortableLibraryIndicators.length, 59, 'portable Prolog library count');
|
|
2821
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
|
|
2748
2822
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
|
2749
2823
|
assertEqual(eyePrologLibraryIndicators.length, 99, 'complete EyeProlog library surface');
|
|
2750
2824
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
@@ -2752,6 +2826,8 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2752
2826
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
|
2753
2827
|
assertEqual(registry.get('eyeprolog__freeze', 2), null, 'private freeze adapter is absent from ISO registry');
|
|
2754
2828
|
assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
|
|
2829
|
+
assertEqual(registry.get('eyeprolog__countall', 2), null, 'private countall adapter is absent from ISO registry');
|
|
2830
|
+
assertEqual(Boolean(library.get('eyeprolog__countall', 2)), true, 'private countall adapter is registered for EyeProlog');
|
|
2755
2831
|
assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'private CLP(Z) labeling adapter is registered');
|
|
2756
2832
|
assertEqual(Boolean(library.get('eyeprolog__clpz_global_cardinality', 3)), true, 'private CLP(Z) cardinality adapter is registered');
|
|
2757
2833
|
assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
|
|
@@ -2807,6 +2883,25 @@ portable_check(A, B, C) :- lowercase('HELLO', A), replace('banana', 'na', 'NA',
|
|
|
2807
2883
|
assertEqual(program.findGroup('local_time', 1), null, 'local_time/1 is absent from the library');
|
|
2808
2884
|
},
|
|
2809
2885
|
},
|
|
2886
|
+
{
|
|
2887
|
+
name: 'countall validates Count before executing Goal and counts without a bag',
|
|
2888
|
+
run: () => {
|
|
2889
|
+
const source = `:- use_module(library(prologue)).
|
|
2890
|
+
item(a).
|
|
2891
|
+
item(b).
|
|
2892
|
+
item(c).
|
|
2893
|
+
candidate :- item(_).
|
|
2894
|
+
`;
|
|
2895
|
+
let caught = null;
|
|
2896
|
+
try {
|
|
2897
|
+
run(source, { goal: 'countall(throw(x), -1)' });
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
caught = error;
|
|
2900
|
+
}
|
|
2901
|
+
assertEqual(caught?.formal, 'domain_error(not_less_than_zero)', 'negative Count error priority');
|
|
2902
|
+
assertEqual(run(source, { goal: 'countall(candidate, N)' }).stdout, 'countall(candidate, 3).\n', 'solution count');
|
|
2903
|
+
},
|
|
2904
|
+
},
|
|
2810
2905
|
{
|
|
2811
2906
|
name: 'ISO extension module provides portable control and collection relations',
|
|
2812
2907
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1831,8 +1831,11 @@ sorted-list operation. No process-global variable registry or creation ordinal
|
|
|
1831
1831
|
is retained or exposed through later comparisons.
|
|
1832
1832
|
|
|
1833
1833
|
EyeProlog periodically checks detectable JavaScript heap use and keeps a quarter
|
|
1834
|
-
of the host heap ceiling in reserve so the solver can unwind and report
|
|
1835
|
-
`resource_error(memory)` before a fatal host out-of-memory abort.
|
|
1834
|
+
of the applicable host heap ceiling in reserve so the solver can unwind and report
|
|
1835
|
+
`resource_error(memory)` before a fatal host out-of-memory abort. When Node is
|
|
1836
|
+
started with `--max-old-space-size`, the guard compares that old-generation
|
|
1837
|
+
ceiling with V8's non-young heap spaces; short-lived new-generation allocations
|
|
1838
|
+
therefore do not cause a false resource error. Embedders may
|
|
1836
1839
|
replace that automatically derived soft ceiling with `maxMemoryBytes`; setting
|
|
1837
1840
|
it to `Infinity` disables the proactive check. Environments that do not expose
|
|
1838
1841
|
heap use cannot provide the proactive check. Host capacity failures that V8
|
|
@@ -5000,7 +5003,10 @@ travel_status(From, To, Status) :-
|
|
|
5000
5003
|
discards alternatives created since entry into the current predicate call.
|
|
5001
5004
|
The two can produce the same first answer without expressing the same control
|
|
5002
5005
|
boundary. Keep cut close to the choice it documents and test the complete
|
|
5003
|
-
answer set before and after introducing it.
|
|
5006
|
+
answer set before and after introducing it. A cut executed inside a predicate
|
|
5007
|
+
called by one disjunction branch remains local to that predicate: if the branch
|
|
5008
|
+
later fails, `Left ; Right` must still try `Right`. This remains true for
|
|
5009
|
+
cut-bearing validation helpers used by generators such as `between/3`.
|
|
5004
5010
|
|
|
5005
5011
|
Exceptions separate an exceptional call from ordinary logical failure:
|
|
5006
5012
|
|
|
@@ -5252,7 +5258,11 @@ error rather than guessing an encoding.
|
|
|
5252
5258
|
`read/1-2` reads the next term. `read_term/2-3` can also return all variables,
|
|
5253
5259
|
source variable names, and singletons. The metadata contains variables, so a
|
|
5254
5260
|
program normally validates or transforms it before placing it in a ground
|
|
5255
|
-
query answer.
|
|
5261
|
+
query answer. Every read operation creates a fresh variable set: a source name
|
|
5262
|
+
such as `X` in two separately read terms does not alias either the caller's `X`
|
|
5263
|
+
or the variable named `X` by the other read. Within one read term, repeated
|
|
5264
|
+
occurrences and the variables returned through its metadata still share as
|
|
5265
|
+
written. `stream_property/2` exposes mode, type, alias, position, and
|
|
5256
5266
|
end state. `current_input/1`, `current_output/1`, `set_input/1`, and
|
|
5257
5267
|
`set_output/1` manage defaults shared by nested goals.
|
|
5258
5268
|
|
|
@@ -5612,7 +5622,7 @@ collision because other Prolog systems commonly reject it while loading.
|
|
|
5612
5622
|
| `\+(+Goal)` | Negation as finite failure. It succeeds once when `Goal` has no solution and never exports bindings made while testing `Goal`. Bind variables needed by the test first. |
|
|
5613
5623
|
| `once(+Goal)` | Returns only the first solution of `Goal`, or fails when there is none. |
|
|
5614
5624
|
| `repeat` | Produces an unbounded sequence of successes; normally paired with a test, cut, exception, or `halt/0`. |
|
|
5615
|
-
| `Left ; Right` | Enumerates `Left`, then `Right`, restoring the incoming environment between branches. |
|
|
5625
|
+
| `Left ; Right` | Enumerates `Left`, then `Right`, restoring the incoming environment between branches. A cut inside a called predicate cannot discard the other branch. |
|
|
5616
5626
|
| `If -> Then` | Commits to the first solution of `If` and runs `Then`; it does not provide an else branch by itself. |
|
|
5617
5627
|
| `(If -> Then ; Else)` | Runs `Then` from the first solution of `If`, otherwise runs `Else`. Alternatives of `If` are discarded. |
|
|
5618
5628
|
| `catch(+Goal,?Catcher,+Recovery)` | Runs `Goal`; on a matching thrown ball or `PrologError`, unifies it with `Catcher` and calls `Recovery`. Runtime errors are exposed as *error(Formal,eyeprolog)*. |
|
|
@@ -7005,7 +7015,7 @@ hand.
|
|
|
7005
7015
|
|
|
7006
7016
|
#### Running and extending the corpus
|
|
7007
7017
|
|
|
7008
|
-
Run all
|
|
7018
|
+
Run all 211 normal answer goldens and the 61 selected proof goldens with:
|
|
7009
7019
|
|
|
7010
7020
|
```sh
|
|
7011
7021
|
npm run test:examples
|
|
@@ -7050,6 +7060,7 @@ syntax. Separate corpora cover expected errors, warnings, and proofs:
|
|
|
7050
7060
|
```sh
|
|
7051
7061
|
npm run test:conformance
|
|
7052
7062
|
npm run test:iso-strict
|
|
7063
|
+
npm run test:wg17-syntax
|
|
7053
7064
|
node test/run-conformance-report.mjs
|
|
7054
7065
|
```
|
|
7055
7066
|
|
|
@@ -7058,14 +7069,16 @@ Part 1 conformance audit. It distinguishes implemented/tested families from
|
|
|
7058
7069
|
requirements whose normative `shall` clauses, option combinations, or error
|
|
7059
7070
|
precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
|
|
7060
7071
|
maps language families to representative executable cases.
|
|
7072
|
+
`test/conformance/WG17-SYNTAX-STATUS.md` separately traces all 366 active
|
|
7073
|
+
upstream syntax cases to exact strict-reader outcomes, with no untraced case.
|
|
7061
7074
|
|
|
7062
7075
|
The complete suite must pass before release. The file-based conformance corpus
|
|
7063
|
-
contains
|
|
7076
|
+
contains 791 cases, including 386 focused ISO
|
|
7064
7077
|
cases derived from the success, failure, mode, and error behavior in
|
|
7065
7078
|
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
7066
|
-
Separate exact-output suites check
|
|
7067
|
-
examples and
|
|
7068
|
-
their declared goals are executed. The
|
|
7079
|
+
Separate exact-output suites check 211 normal
|
|
7080
|
+
examples and 61 proof examples; all extracted book programs are parsed and
|
|
7081
|
+
their declared goals are executed. The eight-case
|
|
7069
7082
|
playground contract suite imports the production worker, sends real reasoning
|
|
7070
7083
|
requests through its message protocol, and crawls the served module graph for
|
|
7071
7084
|
missing assets, bad MIME types, and static Node-only imports. The generated
|
package/why-eyeprolog.md
CHANGED
|
@@ -20,7 +20,8 @@ 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
|
|
23
|
+
behavior, including a complete executable trace of the 366 active WG17 syntax
|
|
24
|
+
cases. This is extensive implementation evidence, not certification by an
|
|
24
25
|
independent standards body.
|
|
25
26
|
|
|
26
27
|
## Why a small implementation?
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer([?]).
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
p(?).
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
parse line 1: bad character "?"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
parse line 1: bad character "?"
|