eyeprolog 1.2.29 → 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.
Files changed (33) hide show
  1. package/conformance-report.md +3 -3
  2. package/examples/output/fast-fourier-transform.pl +1 -1
  3. package/package.json +1 -1
  4. package/src/io.js +73 -5
  5. package/src/iso.js +50 -7
  6. package/src/lib/clpz.pl +13 -13
  7. package/src/lib/iso_ext.pl +1 -3
  8. package/src/lib/lists.pl +1 -1
  9. package/src/lib/prologue.pl +1 -3
  10. package/src/parser.js +24 -21
  11. package/src/repl.js +1 -0
  12. package/src/solver.js +16 -0
  13. package/src/standard-library.js +2 -2
  14. package/src/write.js +6 -3
  15. package/test/conformance/README.md +1 -1
  16. package/test/conformance/cases/atoms/question_mark_graphic_atom.pl +3 -0
  17. package/test/conformance/expected/atoms/question_mark_graphic_atom.pl +1 -0
  18. package/test/conformance/expected/iso/operator_atoms_as_arguments.pl +2 -2
  19. package/test/conformance/expected-errors/syntax/bad_question_variable.txt +1 -1
  20. package/test/conformance/expected-errors/variables/bare_underscore_name_in_compound_rejected.txt +1 -1
  21. package/test/conformance/expected-errors/variables/extra_uppercase_in_list.txt +1 -1
  22. package/test/conformance/expected-errors/variables/question_digit_rejected.txt +1 -1
  23. package/test/conformance/expected-errors/variables/underscore_named_rejected.txt +1 -1
  24. package/test/conformance/expected-errors/variables/uppercase_in_body_rejected.txt +1 -1
  25. package/test/conformance/expected-errors/variables/uppercase_variable_rejected.txt +1 -1
  26. package/test/conformance/wg17-syntax-cases.json +5 -5
  27. package/test/fixtures/prologue_extended_quad.pl +2 -2
  28. package/test/run-regression.mjs +122 -27
  29. package/the-art-of-eyeprolog.md +1 -1
  30. package/test/conformance/errors/variables/bare_underscore_in_list_rejected.pl +0 -1
  31. package/test/conformance/errors/variables/bare_underscore_rejected.pl +0 -1
  32. package/test/conformance/expected-errors/variables/bare_underscore_in_list_rejected.txt +0 -1
  33. package/test/conformance/expected-errors/variables/bare_underscore_rejected.txt +0 -1
@@ -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 | 22 | 8 | 0 | 0 | 30 |
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 | 9 | 0 | 0 | 25 |
26
- | **Total** | **483** | **269** | **19** | **21** | **792** |
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('+', 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))]).
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.29",
6
+ "version": "1.2.30",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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
- if (mode === 'read') content = fs.readFileSync(path, type === 'binary' ? null : 'utf8');
36
- else if (mode === 'append' && fs.existsSync(path)) content = fs.readFileSync(path, type === 'binary' ? null : 'utf8');
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
- const value = stream.type === 'binary' ? stream.content[stream.position] : String(stream.content)[stream.position];
56
- if (!peek) stream.position++;
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
- const unit = solver.io.readUnit(stream, peek);
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 new PrologError('syntax_error(number)');
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 new PrologError('syntax_error(number)');
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,
@@ -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) :- findall(1, Goal, Ones), lists__length_count(Ones, 0, 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]) :-
@@ -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 (this.strictIso && leftIsBareOperatorAtom) {
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 (this.strictIso && leftIsBareOperatorAtom) {
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 (this.strictIso && leftIsBareOperatorAtom && left.type === ATOM && !allowOperatorAtom) {
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 (this.strictIso && !allowOperatorAtom) {
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 (this.strictIso && !allowOperatorAtom && this.token.type !== TOK.DOT) {
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 (this.strictIso && !allowOperatorAtom &&
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 new Error('not exactly one number token');
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 new Error('not exactly one number token');
1413
+ if (secondCode < 0xdc00 || secondCode > 0xdfff) throw invalidNumberTokenError;
1411
1414
  value += source[position++];
1412
1415
  } else if (firstCode >= 0xdc00 && firstCode <= 0xdfff) {
1413
- throw new Error('not exactly one number token');
1416
+ throw invalidNumberTokenError;
1414
1417
  }
1415
1418
 
1416
1419
  if (value === "'") {
1417
- if (source[position] !== "'") throw new Error('not exactly one number token');
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 new Error('not exactly one number token');
1430
+ if (!digits || source[position++] !== '\\') throw invalidNumberTokenError;
1428
1431
  const code = Number.parseInt(digits, 16);
1429
- if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw new Error('not exactly one number token');
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 new Error('not exactly one number token');
1437
+ if (source[position++] !== '\\') throw invalidNumberTokenError;
1435
1438
  const code = Number.parseInt(digits, 8);
1436
- if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw new Error('not exactly one number token');
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 new Error('not exactly one number token');
1444
+ throw invalidNumberTokenError;
1442
1445
  }
1443
1446
  }
1444
1447
 
1445
- if (position !== source.length) throw new Error('not exactly one number token');
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 new Error('not exactly one number token');
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 new Error('not exactly one number token');
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 new Error('not exactly one number token');
1481
+ if (position === exponentStart) throw invalidNumberTokenError;
1479
1482
  }
1480
- if (position !== source.length) throw new Error('not exactly one number token');
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
@@ -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++) {
@@ -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
- 'countall/2', 'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
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('!#$&*+-/<=>@^~\\'.split(''));
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 (/^[!#$&*+\-./<=>@^~\\;:]+$/.test(name)) return name;
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));
@@ -109,7 +109,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
109
109
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
110
110
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
111
111
 
112
- The corpus has 386 cases in `iso/` and 792 file-based conformance cases in
112
+ The corpus has 386 cases in `iso/` and 791 file-based conformance cases in
113
113
  total. The separate strict-reader WG17 matrix has 366 executable dispositions.
114
114
  The generated `conformance-report.md` is the authoritative source for current
115
115
  category totals. Together with regression, documentation-sync, API, example,
@@ -0,0 +1,3 @@
1
+ % ISO 6.5.1: ? is a graphic character and may occur in graphic atoms.
2
+ %% goal: question_mark_graphics(A, B)
3
+ question_mark_graphics(A, B) :- A = ?, B = ?? .
@@ -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: bad character "?"
1
+ parse line 1: expected ), got x
@@ -1 +1 @@
1
- parse line 1: bad character "?"
1
+ parse line 1: expected ), got value
@@ -1 +1 @@
1
- parse line 1: bad character "?"
1
+ parse line 1: expected ], got x
@@ -1 +1 @@
1
- parse line 1: bad character "?"
1
+ parse line 1: expected ), got 1
@@ -1 +1 @@
1
- parse line 1: bad character "?"
1
+ parse line 1: expected ), got _name
@@ -1 +1 @@
1
- parse line 1: bad character "?"
1
+ parse line 1: expected ), got x
@@ -1 +1 @@
1
- parse line 1: bad character "?"
1
+ parse line 1: expected ), got x
@@ -1272,7 +1272,7 @@
1272
1272
  "variables": "[]"
1273
1273
  },
1274
1274
  {
1275
- "output": "'-'(1)",
1275
+ "output": "-(1)",
1276
1276
  "variables": "[]"
1277
1277
  }
1278
1278
  ]
@@ -4069,7 +4069,7 @@
4069
4069
  "variables": "[]"
4070
4070
  },
4071
4071
  {
4072
- "output": "$('+'(a,b))",
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": "'+'(__anon0,__anon1)",
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": "'+'(B,B)",
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": "'-'(a,'-'('-'(b)))",
4249
+ "output": "-(a,-(-(b)))",
4250
4250
  "variables": "[]"
4251
4251
  }
4252
4252
  ]
@@ -82,6 +82,6 @@
82
82
  ?- countall(false, non_integer).
83
83
  type_error(integer, non_integer).
84
84
  ?- countall(G, -1).
85
- instantiation_error.
85
+ domain_error(not_less_than_zero, -1).
86
86
  ?- countall(1, -1).
87
- type_error(callable, 1).
87
+ domain_error(not_less_than_zero, -1).
@@ -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, "X = 0.0, C = ['0', '.', '0'], Y = 0.0, D = ['0', '.', '0'].", 'answer');
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, '?-').\ncurrent_op(1200, xfx, '?-').\n",
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, '?-').\n",
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, '?-').\n",
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|'+'(a,*(b,c))|hello world|'hello world'|+(a,b)|a + b|A|$VAR(0)|pair(Left,Right)emit.\n",
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 === 150000) break;
2682
+ if (++count === 500000) break;
2610
2683
  }
2611
- if (count !== 150000) throw new Error('unexpected answer count: ' + 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: 30000 });
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, '150000', 'distinct number syntax attempts');
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: 'named list allocation heap pressure becomes resource_error(memory)',
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(programText)}, { sourceMetadata: false });
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 goal = parseGoalText(${JSON.stringify(goalText)}, {
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 { [...solver.solve([goal], new Env(), 0)]; } catch (error) { caught = error; }
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
- process.stdout.write(caught.formal);
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, 'resource_error(memory)', 'heap pressure resource error');
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, 152, 'EyeProlog registry contains ISO definitions and private library adapters');
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, 39, 'public native EyeProlog builtin count');
2746
- assertEqual(eyePrologPortableLibraryIndicators.length, 60, 'portable Prolog library count');
2747
- assertEqual(eyePrologNativeLibraryIndicators.length, 39, 'native host library count');
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: () => {
@@ -7073,7 +7073,7 @@ maps language families to representative executable cases.
7073
7073
  upstream syntax cases to exact strict-reader outcomes, with no untraced case.
7074
7074
 
7075
7075
  The complete suite must pass before release. The file-based conformance corpus
7076
- contains 792 cases, including 386 focused ISO
7076
+ contains 791 cases, including 386 focused ISO
7077
7077
  cases derived from the success, failure, mode, and error behavior in
7078
7078
  ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
7079
7079
  Separate exact-output suites check 211 normal
@@ -1 +0,0 @@
1
- parse line 1: bad character "?"
@@ -1 +0,0 @@
1
- parse line 1: bad character "?"