eyeprolog 1.2.4 → 1.2.6
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/conformance-report.md +2 -2
- package/package.json +1 -1
- package/src/iso.js +5 -2
- package/src/parser.js +4 -0
- package/src/repl.js +40 -8
- package/src/write.js +2 -0
- package/test/conformance/ISO-MATRIX.md +1 -1
- package/test/conformance/README.md +1 -1
- package/test/conformance/cases/iso/wg17_syntax_high_risk.pl +5 -1
- package/test/conformance/errors/iso/wg17_invalid_octal_escape.pl +1 -0
- package/test/conformance/expected/iso/wg17_syntax_high_risk.pl +2 -0
- package/test/conformance/expected-errors/iso/wg17_invalid_octal_escape.txt +1 -0
- package/test/run-regression.mjs +60 -0
- package/the-art-of-eyeprolog.md +10 -3
package/conformance-report.md
CHANGED
|
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
11
11
|
| builtins | 11 | 0 | 0 | 0 | 11 |
|
|
12
12
|
| context | 11 | 0 | 0 | 0 | 11 |
|
|
13
13
|
| control | 15 | 0 | 0 | 0 | 15 |
|
|
14
|
-
| iso | 167 |
|
|
14
|
+
| iso | 167 | 210 | 0 | 0 | 377 |
|
|
15
15
|
| lists | 52 | 3 | 0 | 0 | 55 |
|
|
16
16
|
| modules | 2 | 0 | 0 | 0 | 2 |
|
|
17
17
|
| negation | 8 | 0 | 19 | 0 | 27 |
|
|
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
23
23
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
24
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
25
|
| variables | 16 | 9 | 0 | 0 | 25 |
|
|
26
|
-
| **Total** | **482** | **
|
|
26
|
+
| **Total** | **482** | **261** | **19** | **21** | **783** |
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1075,9 +1075,12 @@ function quotedEscapeEnd(source, index) {
|
|
|
1075
1075
|
while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
|
|
1076
1076
|
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
1077
1077
|
}
|
|
1078
|
-
if (/^[0-
|
|
1078
|
+
if (/^[0-9]$/.test(escaped)) {
|
|
1079
1079
|
let cursor = index + 1;
|
|
1080
|
-
|
|
1080
|
+
// Scan all decimal digits here, including 8 and 9. This scanner only
|
|
1081
|
+
// locates the end of a candidate quoted escape; the parser remains
|
|
1082
|
+
// authoritative and rejects non-octal digits.
|
|
1083
|
+
while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
|
|
1081
1084
|
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
1082
1085
|
}
|
|
1083
1086
|
|
package/src/parser.js
CHANGED
|
@@ -303,6 +303,10 @@ class Parser {
|
|
|
303
303
|
if (this.take() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
|
|
304
304
|
return String.fromCodePoint(Number.parseInt(digits, 8));
|
|
305
305
|
}
|
|
306
|
+
// A backslash followed by a decimal digit is numeric-escape syntax, but
|
|
307
|
+
// ISO octal digits are limited to 0..7. Do not reinterpret \8 or \9 as
|
|
308
|
+
// implementation-specific one-character escapes.
|
|
309
|
+
if (/^[0-9]$/.test(escaped)) throw new Error(`parse line ${line}: bad octal escape`);
|
|
306
310
|
return escaped;
|
|
307
311
|
}
|
|
308
312
|
nextToken() {
|
package/src/repl.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
24
24
|
let exitCode = 0;
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
|
-
state.solver.runInitializations();
|
|
27
|
+
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
28
28
|
while (true) {
|
|
29
29
|
const text = await readQuery(reader);
|
|
30
30
|
if (text == null) break;
|
|
@@ -35,7 +35,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
35
35
|
if (!options.isoStrict && isUseModuleGoal(goal)) {
|
|
36
36
|
sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
|
|
37
37
|
state = makeState(engine, sources, output, options, state);
|
|
38
|
-
state.solver.runInitializations();
|
|
38
|
+
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
39
39
|
output.write(' true.\n');
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
@@ -43,7 +43,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
43
43
|
if (consultFiles != null) {
|
|
44
44
|
for (const filename of consultFiles) sources.push(await readSource(filename));
|
|
45
45
|
state = makeState(engine, sources, output, options, state);
|
|
46
|
-
state.solver.runInitializations();
|
|
46
|
+
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
47
47
|
output.write(' true.\n');
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
@@ -142,12 +142,39 @@ class LineReader {
|
|
|
142
142
|
return control;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
suspendForComputation() {
|
|
146
|
+
if (!this.terminal || !this.readline) return false;
|
|
147
|
+
// Node readline installs terminal signal handling while the interface is
|
|
148
|
+
// open. During a synchronous Prolog search that prevents the terminal's
|
|
149
|
+
// normal SIGINT/SIGTSTP actions from taking effect until JavaScript yields.
|
|
150
|
+
// Close readline while the solver is running so Ctrl-C can terminate and
|
|
151
|
+
// Ctrl-Z can suspend an otherwise non-terminating computation immediately.
|
|
152
|
+
this.history = [...this.readline.history];
|
|
153
|
+
this.readline.close();
|
|
154
|
+
this.readline = null;
|
|
155
|
+
this.lines = null;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resumeAfterComputation(suspended) {
|
|
160
|
+
if (suspended && !this.readline) this.open();
|
|
161
|
+
}
|
|
162
|
+
|
|
145
163
|
close() {
|
|
146
164
|
if (this.input.isRaw) this.input.setRawMode(false);
|
|
147
165
|
this.readline?.close();
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
function runWithTerminalSignals(reader, operation) {
|
|
170
|
+
const suspended = reader.suspendForComputation();
|
|
171
|
+
try {
|
|
172
|
+
return operation();
|
|
173
|
+
} finally {
|
|
174
|
+
reader.resumeAfterComputation(suspended);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
151
178
|
function makeState(engine, sources, output, options = {}, previousState = null) {
|
|
152
179
|
const strictIso = options.isoStrict === true;
|
|
153
180
|
const program = engine.Program.parseSources(sources, { strictIso, sourceMetadata: strictIso });
|
|
@@ -253,9 +280,12 @@ function quotedEscapeEnd(source, index) {
|
|
|
253
280
|
while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
|
|
254
281
|
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
255
282
|
}
|
|
256
|
-
if (/^[0-
|
|
283
|
+
if (/^[0-9]$/.test(escaped)) {
|
|
257
284
|
let cursor = index + 1;
|
|
258
|
-
|
|
285
|
+
// Scan all decimal digits here, including 8 and 9. This scanner only
|
|
286
|
+
// locates the end of a candidate quoted escape; the parser remains
|
|
287
|
+
// authoritative and rejects non-octal digits.
|
|
288
|
+
while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
|
|
259
289
|
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
260
290
|
}
|
|
261
291
|
|
|
@@ -366,7 +396,7 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
366
396
|
const solver = state.solver;
|
|
367
397
|
solver.solutionsSeen = 0;
|
|
368
398
|
const solutions = solver.solve([goal], new engine.Env(), 0);
|
|
369
|
-
let current = pullSolution(solver, solutions);
|
|
399
|
+
let current = pullSolution(solver, solutions, reader);
|
|
370
400
|
if (current.error) {
|
|
371
401
|
if (current.error?.name === 'HaltSignal') return { halted: true, code: current.error.code };
|
|
372
402
|
throw current.error;
|
|
@@ -380,7 +410,7 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
380
410
|
let automatic = 0;
|
|
381
411
|
let firstAnswer = true;
|
|
382
412
|
while (!current.result.done) {
|
|
383
|
-
const next = pullSolution(solver, solutions);
|
|
413
|
+
const next = pullSolution(solver, solutions, reader);
|
|
384
414
|
output.write(current.output);
|
|
385
415
|
output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
386
416
|
firstAnswer = false;
|
|
@@ -433,16 +463,18 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
433
463
|
return null;
|
|
434
464
|
}
|
|
435
465
|
|
|
436
|
-
function pullSolution(solver, solutions) {
|
|
466
|
+
function pullSolution(solver, solutions, reader) {
|
|
437
467
|
const stream = solver.io.resolve('user_output');
|
|
438
468
|
const originalWrite = stream?.write;
|
|
439
469
|
let captured = '';
|
|
440
470
|
if (stream) stream.write = (text) => { captured += String(text); };
|
|
471
|
+
const suspended = reader.suspendForComputation();
|
|
441
472
|
try {
|
|
442
473
|
return { result: solutions.next(), output: captured };
|
|
443
474
|
} catch (error) {
|
|
444
475
|
return { error, output: captured };
|
|
445
476
|
} finally {
|
|
477
|
+
reader.resumeAfterComputation(suspended);
|
|
446
478
|
if (stream) stream.write = originalWrite;
|
|
447
479
|
}
|
|
448
480
|
}
|
package/src/write.js
CHANGED
|
@@ -22,6 +22,7 @@ function quoteAtom(name) {
|
|
|
22
22
|
for (const ch of name) {
|
|
23
23
|
if (ch === "'") out += "''";
|
|
24
24
|
else if (ch === '\\') out += '\\\\';
|
|
25
|
+
else if (ch === '\x00') out += '\\0\\';
|
|
25
26
|
else if (ch === '\x07') out += '\\a';
|
|
26
27
|
else if (ch === '\b') out += '\\b';
|
|
27
28
|
else if (ch === '\r') out += '\\r';
|
|
@@ -59,6 +60,7 @@ function writeString(value) {
|
|
|
59
60
|
let out = '"';
|
|
60
61
|
for (const ch of value) {
|
|
61
62
|
if (ch === '"' || ch === '\\') out += `\\${ch}`;
|
|
63
|
+
else if (ch === '\x00') out += '\\0\\';
|
|
62
64
|
else if (ch === '\x07') out += '\\a';
|
|
63
65
|
else if (ch === '\b') out += '\\b';
|
|
64
66
|
else if (ch === '\r') out += '\\r';
|
|
@@ -8,7 +8,7 @@ compliance audit and the remaining work before a full conformance claim.
|
|
|
8
8
|
|
|
9
9
|
| Standard area | Implementation | Representative executable coverage |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, syntax error cases |
|
|
11
|
+
| Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, `wg17_invalid_octal_escape`, syntax error cases |
|
|
12
12
|
| Clause 7 term order and unification | finite-tree unification, identity, standard order, errors | `unification_control_information`, `swipl_occurs_check`, `term_modes_and_ordering`, `logtalk_compare_standard_order` |
|
|
13
13
|
| Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability`, `throw_copies_ball` |
|
|
14
14
|
| 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases |
|
|
@@ -102,7 +102,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
102
102
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
103
103
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
104
104
|
|
|
105
|
-
The corpus has
|
|
105
|
+
The corpus has 377 cases in `iso/` and 783 file-based conformance cases in
|
|
106
106
|
total. The generated `conformance-report.md` is the authoritative source for
|
|
107
107
|
current category totals. Together with regression, documentation-sync, API,
|
|
108
108
|
example, and book-example checks, `npm test` is the release gate.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
% High-risk ISO syntax/write regressions, independently derived from
|
|
2
2
|
% ISO/IEC 13211-1 clauses 6.3, 6.4 and 7.10 and cross-checked against the
|
|
3
|
-
% public WG17 conformity-testing syntax cases (#1, #14-15, #28-31, #33-34).
|
|
3
|
+
% public WG17 conformity-testing syntax cases (#1, #14-15, #28-31, #33-34, #301).
|
|
4
4
|
|
|
5
5
|
%% goal: wg17_numeric_escape
|
|
6
6
|
wg17_numeric_escape :-
|
|
@@ -24,3 +24,7 @@ wg17_operator_precedence :-
|
|
|
24
24
|
%% goal: wg17_canonical_list
|
|
25
25
|
wg17_canonical_list :-
|
|
26
26
|
write_canonical([a]), nl.
|
|
27
|
+
|
|
28
|
+
%% goal: wg17_zero_character_escape
|
|
29
|
+
wg17_zero_character_escape :-
|
|
30
|
+
writeq('\0\'), nl.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
'\8\'.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: bad octal escape
|
package/test/run-regression.mjs
CHANGED
|
@@ -671,6 +671,31 @@ c4 ?- call((!;1)).
|
|
|
671
671
|
assertEqual(result.stderr, '', 'stderr');
|
|
672
672
|
},
|
|
673
673
|
},
|
|
674
|
+
{
|
|
675
|
+
name: 'writeq preserves the NUL character with an ISO octal escape',
|
|
676
|
+
run: () => {
|
|
677
|
+
const result = runCli([], {
|
|
678
|
+
input: "writeq('\\0\\').\nhalt.\n",
|
|
679
|
+
});
|
|
680
|
+
assertEqual(result.status, 0, 'exit status');
|
|
681
|
+
assertEqual(result.stdout, "?- '\\0\\' true.\n?- ", 'stdout');
|
|
682
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: 'REPL rejects non-octal numeric escapes without waiting for continuation',
|
|
687
|
+
run: () => {
|
|
688
|
+
const result = runCli([], {
|
|
689
|
+
input: "'\\8\\'.\nhalt.\n",
|
|
690
|
+
});
|
|
691
|
+
assertEqual(result.status, 0, 'exit status');
|
|
692
|
+
assertEqual(result.stdout,
|
|
693
|
+
'?- parse line 1: bad octal escape.\n' +
|
|
694
|
+
'?- ',
|
|
695
|
+
'stdout');
|
|
696
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
697
|
+
},
|
|
698
|
+
},
|
|
674
699
|
{
|
|
675
700
|
name: 'REPL read predicates consume following interactive term input',
|
|
676
701
|
run: () => {
|
|
@@ -694,6 +719,27 @@ c4 ?- call((!;1)).
|
|
|
694
719
|
assertEqual(result.stderr, '', 'stderr');
|
|
695
720
|
},
|
|
696
721
|
},
|
|
722
|
+
{
|
|
723
|
+
name: 'REPL releases terminal signals while a query computes',
|
|
724
|
+
run: () => {
|
|
725
|
+
if (process.platform === 'win32') return;
|
|
726
|
+
const available = spawnSync('sh', ['-c',
|
|
727
|
+
'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
|
|
728
|
+
if (available.status !== 0) return;
|
|
729
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
730
|
+
const scriptCommand =
|
|
731
|
+
`{ printf 'repeat, fail.\n'; sleep 0.2; printf '\\003'; } | ` +
|
|
732
|
+
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
733
|
+
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
734
|
+
cwd: packageRoot,
|
|
735
|
+
encoding: 'utf8',
|
|
736
|
+
timeout: 3000,
|
|
737
|
+
});
|
|
738
|
+
assertEqual(result.error?.code, undefined, 'terminal interrupt timeout');
|
|
739
|
+
assertEqual(result.status, 130, 'SIGINT exit status');
|
|
740
|
+
assertIncludes(result.stdout, '?- repeat, fail.', 'terminal query echo');
|
|
741
|
+
},
|
|
742
|
+
},
|
|
697
743
|
{
|
|
698
744
|
name: 'REPL accepts multiline period-terminated queries',
|
|
699
745
|
run: () => {
|
|
@@ -1312,6 +1358,16 @@ c4 ?- call((!;1)).
|
|
|
1312
1358
|
"answer('\\a').\n",
|
|
1313
1359
|
'read/1 user_input numeric escape',
|
|
1314
1360
|
);
|
|
1361
|
+
|
|
1362
|
+
const invalidOctal = String.fromCharCode(39, 92, 56, 92, 39, 46);
|
|
1363
|
+
assertEqual(
|
|
1364
|
+
run('answer(T) :- catch(read(T), E, T=E).\n', {
|
|
1365
|
+
goal: 'answer(T)',
|
|
1366
|
+
ioOptions: { input: invalidOctal },
|
|
1367
|
+
}).stdout,
|
|
1368
|
+
'answer(error(syntax_error(read_term), eyeprolog)).\n',
|
|
1369
|
+
'read/1 rejects non-octal numeric escape',
|
|
1370
|
+
);
|
|
1315
1371
|
},
|
|
1316
1372
|
},
|
|
1317
1373
|
{
|
|
@@ -3074,6 +3130,10 @@ function between(text, startMarker, endMarker) {
|
|
|
3074
3130
|
return text.slice(contentStart, end);
|
|
3075
3131
|
}
|
|
3076
3132
|
|
|
3133
|
+
function shellQuote(value) {
|
|
3134
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3077
3137
|
function runCli(args, options = {}) {
|
|
3078
3138
|
return spawnSync(process.execPath, [bin, ...args], {
|
|
3079
3139
|
cwd: packageRoot,
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5279,7 +5279,10 @@ message("café").
|
|
|
5279
5279
|
Inside a quoted atom, a single quote is doubled: `'don''t'`. Quoted characters
|
|
5280
5280
|
support the ISO symbolic control escapes such as `\a`, `\n`, and `\t`, and numeric
|
|
5281
5281
|
octal or hexadecimal escapes are terminated by a backslash; for example, `'\7\'`
|
|
5282
|
-
and `'\x7\'` both denote the alert character.
|
|
5282
|
+
and `'\x7\'` both denote the alert character. The NUL character, when present in
|
|
5283
|
+
the processor character set, is written readably as `'\0\'`; digits `8` and `9`
|
|
5284
|
+
are not octal digits, so forms such as `'\8\'` are syntax errors rather than
|
|
5285
|
+
continuation input. Double-quoted lists use the same
|
|
5283
5286
|
quoted-character escapes. Whitespace is insignificant
|
|
5284
5287
|
between tokens, and a `%` comment continues to the end of its line. Doubling
|
|
5285
5288
|
the active delimiter is also accepted inside either quoted form, so `""`
|
|
@@ -6213,7 +6216,11 @@ may span lines and end with a full stop, as in Scryer Prolog:
|
|
|
6213
6216
|
When another answer exists in an interactive terminal, press `;`, Space, or
|
|
6214
6217
|
`n` to ask for it immediately; no Return is needed. Return or `.` stops
|
|
6215
6218
|
enumeration, `a` enumerates all remaining answers, `f` enumerates the next
|
|
6216
|
-
five, and `h` displays the answer-control help.
|
|
6219
|
+
five, and `h` displays the answer-control help. While a query is actively
|
|
6220
|
+
computing, EyeProlog releases readline's terminal signal handling: `Ctrl-C`
|
|
6221
|
+
therefore terminates the current EyeProlog process immediately, and on POSIX
|
|
6222
|
+
terminals `Ctrl-Z` suspends it in the usual shell-managed way. This remains a
|
|
6223
|
+
host top-level convention rather than an ISO/IEC 13211-1 language feature. A
|
|
6217
6224
|
period-terminated query with no solutions prints `false.`; a solution without
|
|
6218
6225
|
visible variable bindings prints `true.`. Answer substitutions are rendered as
|
|
6219
6226
|
valid Prolog syntax under the current operator table: when a bound value would
|
|
@@ -6946,7 +6953,7 @@ precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
|
|
|
6946
6953
|
maps language families to representative executable cases.
|
|
6947
6954
|
|
|
6948
6955
|
The complete suite must pass before release. The file-based conformance corpus
|
|
6949
|
-
contains
|
|
6956
|
+
contains 783 cases, including 377 focused ISO
|
|
6950
6957
|
cases derived from the success, failure, mode, and error behavior in
|
|
6951
6958
|
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
6952
6959
|
Separate exact-output suites check 189 normal
|