eyeprolog 1.2.39 → 1.2.41
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/package.json +1 -1
- package/src/iso.js +24 -6
- package/src/parser.js +0 -4
- package/src/repl.js +20 -17
- package/src/syntax-scan.js +40 -12
- package/src/write.js +7 -3
- package/test/conformance/expected-errors/rules/extra_rule_without_body.txt +1 -1
- package/test/conformance/expected-errors/syntax/extra_double_period_rejected.txt +1 -1
- package/test/conformance/wg17-syntax-cases.json +7 -7
- package/test/run-regression.mjs +104 -9
- package/the-art-of-eyeprolog.md +16 -10
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1074,8 +1074,16 @@ function* nlBuiltin({ solver, goal, env }) {
|
|
|
1074
1074
|
yield env;
|
|
1075
1075
|
}
|
|
1076
1076
|
|
|
1077
|
-
function
|
|
1077
|
+
function activeCharConverter(solver) {
|
|
1078
|
+
if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
return (character) => solver.charConversions.get(character) ?? character;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function* termTextCandidates(stream, solver) {
|
|
1078
1085
|
const source = String(stream.content);
|
|
1086
|
+
const convert = activeCharConverter(solver);
|
|
1079
1087
|
let quote = null, lineComment = false, blockComment = false;
|
|
1080
1088
|
for (let i = stream.position; i < source.length; i++) {
|
|
1081
1089
|
const ch = source[i], next = source[i + 1];
|
|
@@ -1105,15 +1113,25 @@ function* termTextCandidates(stream) {
|
|
|
1105
1113
|
continue;
|
|
1106
1114
|
}
|
|
1107
1115
|
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
1108
|
-
if (
|
|
1116
|
+
if (isTerminatingFullStop(source, i, convert)) {
|
|
1109
1117
|
yield { text: source.slice(stream.position, i + 1), end: i + 1 };
|
|
1110
1118
|
}
|
|
1111
1119
|
}
|
|
1112
1120
|
}
|
|
1113
1121
|
function hasNonLayoutRemainder(source, start) {
|
|
1114
|
-
return source
|
|
1115
|
-
|
|
1116
|
-
|
|
1122
|
+
return lastNonLayoutIndex(source, start) >= start;
|
|
1123
|
+
}
|
|
1124
|
+
function lastNonLayoutIndex(source, start = 0) {
|
|
1125
|
+
const ignored = /[\u0009-\u000d\u0020]+|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g;
|
|
1126
|
+
ignored.lastIndex = start;
|
|
1127
|
+
let cursor = start;
|
|
1128
|
+
let last = -1;
|
|
1129
|
+
for (let match = ignored.exec(source); match != null; match = ignored.exec(source)) {
|
|
1130
|
+
if (match.index > cursor) last = match.index - 1;
|
|
1131
|
+
cursor = match.index + match[0].length;
|
|
1132
|
+
}
|
|
1133
|
+
if (cursor < source.length) last = source.length - 1;
|
|
1134
|
+
return last;
|
|
1117
1135
|
}
|
|
1118
1136
|
function convertedTermText(text, solver) {
|
|
1119
1137
|
if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
|
|
@@ -1207,7 +1225,7 @@ function readTermFromStream(stream, solver) {
|
|
|
1207
1225
|
let requestedInteractiveTerm = false;
|
|
1208
1226
|
while (true) {
|
|
1209
1227
|
let sawCandidate = false;
|
|
1210
|
-
for (const candidate of termTextCandidates(stream)) {
|
|
1228
|
+
for (const candidate of termTextCandidates(stream, solver)) {
|
|
1211
1229
|
sawCandidate = true;
|
|
1212
1230
|
if (candidate.lexicalError) {
|
|
1213
1231
|
stream.position = candidate.end;
|
package/src/parser.js
CHANGED
|
@@ -378,10 +378,6 @@ class Parser {
|
|
|
378
378
|
const line = this.line;
|
|
379
379
|
const ch = this.peek();
|
|
380
380
|
if (!ch) return { type: TOK.EOF, text: '', line };
|
|
381
|
-
if (this.source.startsWith('...', this.pos) && this.peek(3) !== '.') {
|
|
382
|
-
this.pos += 3;
|
|
383
|
-
return { type: TOK.ATOM, text: '...', line };
|
|
384
|
-
}
|
|
385
381
|
if (ch === '?' && this.peek(1) === '-' &&
|
|
386
382
|
!(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
|
|
387
383
|
this.pos += 2;
|
package/src/repl.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import { readSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import { formalErrorTerm
|
|
6
|
+
import { formalErrorTerm } from './iso.js';
|
|
7
7
|
import {
|
|
8
8
|
characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
|
|
9
9
|
} from './syntax-scan.js';
|
|
@@ -171,8 +171,8 @@ class LineReader {
|
|
|
171
171
|
const line = this.readTerminalLineSync();
|
|
172
172
|
if (line == null) return source.trim() ? source : null;
|
|
173
173
|
source += `${line}\n`;
|
|
174
|
-
const end = terminalFullStop(source);
|
|
175
|
-
if (end >= 0
|
|
174
|
+
const end = terminalFullStop(source, solver);
|
|
175
|
+
if (end >= 0) {
|
|
176
176
|
return source.slice(0, end + 1) + '\n';
|
|
177
177
|
}
|
|
178
178
|
prompt = '| ';
|
|
@@ -300,7 +300,7 @@ async function prepareInteractiveTermInput(state, goal, reader) {
|
|
|
300
300
|
// fallback for piped/non-TTY REPL tests and scripted input.
|
|
301
301
|
if (reader.canReadTermSynchronously()) return;
|
|
302
302
|
const stream = interactiveTermInputStream(state, goal);
|
|
303
|
-
if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
|
|
303
|
+
if (stream == null || terminalFullStop(String(stream.content).slice(stream.position), state.solver) >= 0) return;
|
|
304
304
|
|
|
305
305
|
const text = await readInteractiveTerm(reader, state.solver);
|
|
306
306
|
if (text == null) return;
|
|
@@ -343,24 +343,23 @@ async function readInteractiveTerm(reader, solver = null) {
|
|
|
343
343
|
const line = await reader.read(prompt);
|
|
344
344
|
if (line == null) return source.trim() ? source : null;
|
|
345
345
|
source += `${line}\n`;
|
|
346
|
-
const end = terminalFullStop(source);
|
|
347
|
-
if (end >= 0
|
|
346
|
+
const end = terminalFullStop(source, solver);
|
|
347
|
+
if (end >= 0) {
|
|
348
348
|
return source.slice(0, end + 1) + '\n';
|
|
349
349
|
}
|
|
350
350
|
prompt = '| ';
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return !continuesGraphicToken(source, end) || solver == null ||
|
|
360
|
-
isCompleteReadTermText(source.slice(0, end + 1), solver);
|
|
354
|
+
function activeCharConverter(solver) {
|
|
355
|
+
if (solver?.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return (character) => solver.charConversions.get(character) ?? character;
|
|
361
359
|
}
|
|
362
360
|
|
|
363
|
-
function terminalFullStop(source) {
|
|
361
|
+
function terminalFullStop(source, solver = null) {
|
|
362
|
+
const convert = activeCharConverter(solver);
|
|
364
363
|
let quote = null;
|
|
365
364
|
let lineComment = false;
|
|
366
365
|
let blockComment = false;
|
|
@@ -415,7 +414,7 @@ function terminalFullStop(source) {
|
|
|
415
414
|
}
|
|
416
415
|
if ('([{'.includes(ch)) depth++;
|
|
417
416
|
else if (')]}'.includes(ch)) depth = Math.max(0, depth - 1);
|
|
418
|
-
else if (
|
|
417
|
+
else if (depth === 0 && isTerminatingFullStop(source, i, convert) &&
|
|
419
418
|
onlyLayoutAndComments(source.slice(i + 1))) return i;
|
|
420
419
|
}
|
|
421
420
|
return -1;
|
|
@@ -503,11 +502,15 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
503
502
|
if (formattingAfterAdvance) output.write(' ');
|
|
504
503
|
formattingAfterAdvance = false;
|
|
505
504
|
output.write(current.output);
|
|
506
|
-
|
|
505
|
+
const answer = formatAnswer(engine, state, variables, current.result.value);
|
|
506
|
+
output.write(`${firstAnswer ? ' ' : ''}${answer}`);
|
|
507
507
|
answersShown++;
|
|
508
508
|
firstAnswer = false;
|
|
509
509
|
if (!next.error && next.result.done) {
|
|
510
|
-
|
|
510
|
+
// A terminal full stop cannot immediately follow a graphic token: the
|
|
511
|
+
// scanner would absorb it into that token. Insert layout so the printed
|
|
512
|
+
// answer remains valid Prolog text (issue #44).
|
|
513
|
+
output.write(`${continuesGraphicToken(answer, answer.length) ? ' ' : ''}.\n`);
|
|
511
514
|
return null;
|
|
512
515
|
}
|
|
513
516
|
|
package/src/syntax-scan.js
CHANGED
|
@@ -4,21 +4,49 @@
|
|
|
4
4
|
|
|
5
5
|
const graphicTokenCharacters = new Set('#$&*+-./<=>?@^~\\:');
|
|
6
6
|
|
|
7
|
-
export function continuesGraphicToken(source, index) {
|
|
8
|
-
|
|
7
|
+
export function continuesGraphicToken(source, index, convert = null) {
|
|
8
|
+
if (index <= 0) return false;
|
|
9
|
+
const rawPrevious = source[index - 1];
|
|
10
|
+
const previous = convert == null ? rawPrevious : convert(rawPrevious);
|
|
11
|
+
// Most full stops follow a non-graphic token. Reject those in O(1) before
|
|
12
|
+
// doing the rarer character-code/comment disambiguation below; otherwise a
|
|
13
|
+
// large source with many term-ending dots degenerates into repeated backward
|
|
14
|
+
// scans (notably multi-megabyte generated data files).
|
|
15
|
+
if (!graphicTokenCharacters.has(previous)) return false;
|
|
16
|
+
|
|
17
|
+
// A graphic-looking character can be the payload or closing escape of a
|
|
18
|
+
// character-code constant rather than a graphic token. For example, the
|
|
19
|
+
// backslash immediately before the full stop in `0'\x41\.` belongs to
|
|
20
|
+
// the number token, so that full stop still terminates the term.
|
|
21
|
+
const apostrophe = source.lastIndexOf("'", index - 1);
|
|
22
|
+
if (apostrophe >= 0 && characterCodeConstantEnd(source, apostrophe) === index - 1) return false;
|
|
23
|
+
// The slash that closes a bracketed comment is layout, not the tail of a
|
|
24
|
+
// graphic token. Distinguish it from spellings such as `//*.*/`, where the
|
|
25
|
+
// apparent /* is itself embedded in a graphic token and therefore never
|
|
26
|
+
// opens a comment.
|
|
27
|
+
if (source[index - 1] === '/' && source[index - 2] === '*') {
|
|
28
|
+
for (let open = source.lastIndexOf('/*', index - 3); open >= 0;
|
|
29
|
+
open = source.lastIndexOf('/*', open - 1)) {
|
|
30
|
+
if (open === 0 || !graphicTokenCharacters.has(source[open - 1])) return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
9
34
|
}
|
|
10
35
|
|
|
11
|
-
export function isTerminatingFullStop(source, index) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
36
|
+
export function isTerminatingFullStop(source, index, convert = null) {
|
|
37
|
+
const current = convert == null ? source[index] : convert(source[index]);
|
|
38
|
+
if (current !== '.') return false;
|
|
39
|
+
const rawNext = source[index + 1] ?? '';
|
|
40
|
+
const next = convert == null ? rawNext : convert(rawNext);
|
|
41
|
+
// A full stop cannot terminate a term when it can still extend the graphic
|
|
42
|
+
// token immediately before it. This remains true at a line boundary and at
|
|
43
|
+
// the current end of interactive input: `*.\n` is the graphic token `*.`
|
|
44
|
+
// followed by layout, so read/1 must keep waiting for a separate end char.
|
|
45
|
+
// Conversely `!.\n` terminates because ! is a solo token, not a graphic
|
|
46
|
+
// token character accepted by continuesGraphicToken().
|
|
47
|
+
if (continuesGraphicToken(source, index, convert)) return false;
|
|
20
48
|
if (next === '' || next === '%' || next === '\n' || next === '\r') return true;
|
|
21
|
-
if (/^[\u0009\u000b\u000c\u0020]$/.test(next)) return
|
|
49
|
+
if (/^[\u0009\u000b\u000c\u0020]$/.test(next)) return true;
|
|
22
50
|
return false;
|
|
23
51
|
}
|
|
24
52
|
|
package/src/write.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
Env, deref, isCons, isEmptyList,
|
|
5
5
|
} from './term.js';
|
|
6
6
|
|
|
7
|
-
const graphicAtomCharacters = new Set('
|
|
8
|
-
const dottedGraphicAtomCharacters =
|
|
7
|
+
const graphicAtomCharacters = new Set('!#$&*+-./<=>?@^~\\'.split(''));
|
|
8
|
+
const dottedGraphicAtomCharacters = graphicAtomCharacters;
|
|
9
9
|
const compactInfixOperators = new Set([':', '..']);
|
|
10
10
|
|
|
11
11
|
function quotedControlEscape(ch) {
|
|
@@ -28,7 +28,11 @@ function quotedControlEscape(ch) {
|
|
|
28
28
|
function atomNeedsQuotes(name) {
|
|
29
29
|
if (!name) return true;
|
|
30
30
|
if (name === '[]' || name === '{}') return false;
|
|
31
|
-
|
|
31
|
+
// A lone full stop is the end token, not a graphic atom. Longer
|
|
32
|
+
// graphic tokens may contain dots and are valid unquoted writeq/1 output
|
|
33
|
+
// (WG17 #371-373: ./*, .*, ...*). Only a token beginning with /* would
|
|
34
|
+
// be read as a bracketed comment and therefore still requires quoting.
|
|
35
|
+
if (name === '.') return true;
|
|
32
36
|
if (name.startsWith('/*')) return true;
|
|
33
37
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
|
|
34
38
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ., got :-.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1: expected ., got
|
|
1
|
+
parse line 1: expected ., got ..
|
|
@@ -671,7 +671,7 @@
|
|
|
671
671
|
"type": "success",
|
|
672
672
|
"stages": [
|
|
673
673
|
{
|
|
674
|
-
"output": "
|
|
674
|
+
"output": "//*.*/",
|
|
675
675
|
"variables": "[]"
|
|
676
676
|
}
|
|
677
677
|
]
|
|
@@ -4302,7 +4302,7 @@
|
|
|
4302
4302
|
"variables": "[]"
|
|
4303
4303
|
},
|
|
4304
4304
|
{
|
|
4305
|
-
"output": ">(
|
|
4305
|
+
"output": ">(>.(a),b)",
|
|
4306
4306
|
"variables": "[]"
|
|
4307
4307
|
}
|
|
4308
4308
|
]
|
|
@@ -4322,7 +4322,7 @@
|
|
|
4322
4322
|
"variables": "[]"
|
|
4323
4323
|
},
|
|
4324
4324
|
{
|
|
4325
|
-
"output": "=(
|
|
4325
|
+
"output": "=(>.(a),b)",
|
|
4326
4326
|
"variables": "[]"
|
|
4327
4327
|
}
|
|
4328
4328
|
]
|
|
@@ -4342,7 +4342,7 @@
|
|
|
4342
4342
|
"variables": "[]"
|
|
4343
4343
|
},
|
|
4344
4344
|
{
|
|
4345
|
-
"output": "','(
|
|
4345
|
+
"output": "','(>.(a),b)",
|
|
4346
4346
|
"variables": "[]"
|
|
4347
4347
|
}
|
|
4348
4348
|
]
|
|
@@ -4362,7 +4362,7 @@
|
|
|
4362
4362
|
"variables": "[]"
|
|
4363
4363
|
},
|
|
4364
4364
|
{
|
|
4365
|
-
"output": "
|
|
4365
|
+
"output": ">.(a)",
|
|
4366
4366
|
"variables": "[]"
|
|
4367
4367
|
}
|
|
4368
4368
|
]
|
|
@@ -5022,7 +5022,7 @@
|
|
|
5022
5022
|
"type": "success",
|
|
5023
5023
|
"stages": [
|
|
5024
5024
|
{
|
|
5025
|
-
"output": "
|
|
5025
|
+
"output": ".+",
|
|
5026
5026
|
"variables": "[]"
|
|
5027
5027
|
}
|
|
5028
5028
|
]
|
|
@@ -5273,7 +5273,7 @@
|
|
|
5273
5273
|
"variables": "[]"
|
|
5274
5274
|
},
|
|
5275
5275
|
{
|
|
5276
|
-
"output": "
|
|
5276
|
+
"output": ".>(.>(a))",
|
|
5277
5277
|
"variables": "[]"
|
|
5278
5278
|
}
|
|
5279
5279
|
]
|
package/test/run-regression.mjs
CHANGED
|
@@ -102,6 +102,16 @@ export function runRegression(reporter = new TestReporter()) {
|
|
|
102
102
|
|
|
103
103
|
function regressionCases() {
|
|
104
104
|
return [
|
|
105
|
+
{
|
|
106
|
+
name: 'large source scanning avoids quadratic full-stop lookback',
|
|
107
|
+
run: () => {
|
|
108
|
+
const result = runCli(['examples/path-discovery.pl'], { timeout: 10000 });
|
|
109
|
+
if (result.error) throw new Error(`path-discovery timed out or failed to launch: ${result.error.message}`);
|
|
110
|
+
assertEqual(result.status, 0, `path-discovery status; stderr=${result.stderr}`);
|
|
111
|
+
assertIncludes(result.stdout, "airroute('Ostend-Bruges International Airport', 'Václav Havel Airport Prague'",
|
|
112
|
+
'path-discovery result');
|
|
113
|
+
},
|
|
114
|
+
},
|
|
105
115
|
{
|
|
106
116
|
name: '--proof rule fact explanation output',
|
|
107
117
|
run: () => runWhy({
|
|
@@ -733,24 +743,77 @@ c4 ?- call((!;1)).
|
|
|
733
743
|
assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
|
|
734
744
|
},
|
|
735
745
|
},
|
|
746
|
+
{
|
|
747
|
+
name: 'top level separates terminal full stop from graphic answers (issue #44)',
|
|
748
|
+
run: () => {
|
|
749
|
+
const result = runCli([], { input: 'X = .* .\nhalt.\n' });
|
|
750
|
+
assertEqual(result.status, 0, 'issue #44 exit status');
|
|
751
|
+
assertIncludes(result.stdout, 'X = .* .\n', 'graphic binding is separated from terminal full stop');
|
|
752
|
+
assertNotIncludes(result.stdout, 'X = .*.\n', 'terminal full stop is not absorbed into graphic atom');
|
|
753
|
+
assertEqual(result.stderr, '', 'issue #44 stderr');
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: 'writeq leaves ISO dotted graphic atoms unquoted (WG17 #371-373)',
|
|
758
|
+
run: () => {
|
|
759
|
+
const result = runCli([], {
|
|
760
|
+
input: 'writeq(./*).\nwriteq(.*).\nwriteq(...*).\nhalt.\n',
|
|
761
|
+
});
|
|
762
|
+
assertEqual(result.status, 0, 'dotted graphic writeq exit status');
|
|
763
|
+
assertIncludes(result.stdout, ' ./* true.\n', 'writeq ./* is unquoted');
|
|
764
|
+
assertIncludes(result.stdout, ' .* true.\n', 'writeq .* is unquoted');
|
|
765
|
+
assertIncludes(result.stdout, ' ...* true.\n', 'writeq ...* is unquoted');
|
|
766
|
+
assertNotIncludes(result.stdout, "'./*'", 'writeq ./* has no quotes');
|
|
767
|
+
assertNotIncludes(result.stdout, "'.*'", 'writeq .* has no quotes');
|
|
768
|
+
assertNotIncludes(result.stdout, "'...*'", 'writeq ...* has no quotes');
|
|
769
|
+
},
|
|
770
|
+
},
|
|
736
771
|
{
|
|
737
772
|
name: 'readers distinguish graphic tokens, comments, and full stops (issue #41)',
|
|
738
773
|
run: () => {
|
|
739
|
-
const parsed = parseProgramText('
|
|
774
|
+
const parsed = parseProgramText('./* .');
|
|
740
775
|
assertEqual(parsed.length, 1, 'graphic atom clause count');
|
|
741
776
|
assertEqual(parsed[0].head.name, './*', 'comment opener stays inside graphic atom');
|
|
742
777
|
|
|
778
|
+
// A dot immediately after a graphic token belongs to that maximal
|
|
779
|
+
// token. A separate end char is therefore required even at a line
|
|
780
|
+
// boundary; this is the waiting behavior called out in WG17 #370-373.
|
|
781
|
+
const waitProgram = Program.parse('');
|
|
782
|
+
const waitSolver = new Solver(waitProgram, {
|
|
783
|
+
registry: getEyePrologRegistry(),
|
|
784
|
+
ioOptions: { input: '*.\n' },
|
|
785
|
+
});
|
|
786
|
+
const waitStream = waitSolver.io.resolve('user_input');
|
|
787
|
+
let refillRequests = 0;
|
|
788
|
+
waitStream.interactiveReadTerm = () => {
|
|
789
|
+
refillRequests++;
|
|
790
|
+
return '.\n';
|
|
791
|
+
};
|
|
792
|
+
const waitGoal = parseGoalText('read(T)', {
|
|
793
|
+
operatorDefinitions: [...waitProgram.operators.values()],
|
|
794
|
+
});
|
|
795
|
+
const waitAnswers = [...waitSolver.solve([waitGoal], new Env(), 0)];
|
|
796
|
+
assertEqual(waitAnswers.length, 1, 'graphic token read answer after refill');
|
|
797
|
+
assertEqual(refillRequests, 1, 'graphic token boundary waits for a separate end char');
|
|
798
|
+
assertEqual(copyResolved(waitGoal.args[0], waitAnswers[0]).name, '*.', 'maximal graphic token after refill');
|
|
799
|
+
|
|
743
800
|
const read = runEyeProlog('', {
|
|
744
801
|
goal: 'read(T)',
|
|
745
|
-
ioOptions: { input: './*.' },
|
|
802
|
+
ioOptions: { input: './*. .' },
|
|
803
|
+
});
|
|
804
|
+
assertEqual(read.stdout, "read(./*.).\n", 'dotted graphic atom writeq readback');
|
|
805
|
+
|
|
806
|
+
const ellipsisGraphic = runEyeProlog('', {
|
|
807
|
+
goal: 'read(T)',
|
|
808
|
+
ioOptions: { input: '...*\n.\n' },
|
|
746
809
|
});
|
|
747
|
-
assertEqual(
|
|
810
|
+
assertEqual(ellipsisGraphic.stdout, "read(...*).\n", 'ellipsis prefix remains inside maximal graphic atom');
|
|
748
811
|
|
|
749
812
|
const consecutive = runEyeProlog('answer(A, B) :- read(A), read(B).\n', {
|
|
750
813
|
goal: 'answer(A, B)',
|
|
751
814
|
ioOptions: { input: './*. .\nok.\n' },
|
|
752
815
|
});
|
|
753
|
-
assertEqual(consecutive.stdout, "answer(
|
|
816
|
+
assertEqual(consecutive.stdout, "answer(./*., ok).\n", 'following read starts after the complete term');
|
|
754
817
|
|
|
755
818
|
// A possible full stop can fail to complete the term while still
|
|
756
819
|
// extending a current graphic operator into an ordinary atom. The
|
|
@@ -777,6 +840,37 @@ c4 ?- call((!;1)).
|
|
|
777
840
|
assertEqual(term.args[1].name, `${name}.`, `graphic operator atom for ${name}`);
|
|
778
841
|
}
|
|
779
842
|
|
|
843
|
+
// Unlike an interactive reader waiting at a line boundary, a buffered
|
|
844
|
+
// stream can see later non-layout input. Its first dot therefore
|
|
845
|
+
// remains in the maximal graphic token, making the adjacent ! invalid
|
|
846
|
+
// rather than prematurely returning the shorter atom.
|
|
847
|
+
for (const name of [...graphicOperators, '?', '#', '@', './*', '//*']) {
|
|
848
|
+
let bufferedError = null;
|
|
849
|
+
try {
|
|
850
|
+
runEyeProlog('', {
|
|
851
|
+
goal: 'read_term(T, [])',
|
|
852
|
+
ioOptions: { input: `${name}.\n!\n.` },
|
|
853
|
+
});
|
|
854
|
+
} catch (caught) {
|
|
855
|
+
bufferedError = caught;
|
|
856
|
+
}
|
|
857
|
+
assertEqual(
|
|
858
|
+
bufferedError?.message,
|
|
859
|
+
'error(syntax_error(read_term))',
|
|
860
|
+
`buffered graphic atom boundary for ${name}`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const bufferedPath = path.join(tmp, 'graphic-atom-boundary.pl');
|
|
865
|
+
fs.writeFileSync(bufferedPath, '*.\n!\n.');
|
|
866
|
+
const namedStream = runEyeProlog([
|
|
867
|
+
`caught(ok) :- open(${sourceAtom(bufferedPath)}, read, S),`,
|
|
868
|
+
' catch(read_term(S, _, []), error(syntax_error(read_term), _), true),',
|
|
869
|
+
' close(S).',
|
|
870
|
+
'',
|
|
871
|
+
].join('\n'), { goal: 'caught(ok)' });
|
|
872
|
+
assertEqual(namedStream.stdout, 'caught(ok).\n', 'named buffered stream syntax error');
|
|
873
|
+
|
|
780
874
|
let error = null;
|
|
781
875
|
try {
|
|
782
876
|
runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
|
|
@@ -789,7 +883,7 @@ c4 ?- call((!;1)).
|
|
|
789
883
|
input: 'read(T).\n./*. .\nread(T).\nok.\nread(T).\n!.!.\nhalt.\n',
|
|
790
884
|
});
|
|
791
885
|
assertEqual(repl.status, 0, 'REPL exit status');
|
|
792
|
-
assertIncludes(repl.stdout, 'T =
|
|
886
|
+
assertIncludes(repl.stdout, 'T = ./*. .', 'REPL dotted graphic atom answer');
|
|
793
887
|
assertNotIncludes(repl.stdout, "T = './*.'", 'REPL dotted graphic atom has no spurious quotes');
|
|
794
888
|
assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
|
|
795
889
|
assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
|
|
@@ -1531,10 +1625,10 @@ c4 ?- call((!;1)).
|
|
|
1531
1625
|
if (available.status !== 0) return;
|
|
1532
1626
|
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
1533
1627
|
const scriptCommand =
|
|
1534
|
-
`{ printf 'read(X), read(Y).\n'; sleep 0.
|
|
1535
|
-
`printf 'foo.\n'; sleep 0.
|
|
1536
|
-
`printf 'read(Z).\n'; sleep 0.
|
|
1537
|
-
`printf 'true.\n'; sleep 0.
|
|
1628
|
+
`{ printf 'read(X), read(Y).\n'; sleep 0.3; ` +
|
|
1629
|
+
`printf 'foo.\n'; sleep 0.3; printf 'bar.\n'; sleep 0.3; ` +
|
|
1630
|
+
`printf 'read(Z).\n'; sleep 0.3; printf '\\004'; sleep 0.3; ` +
|
|
1631
|
+
`printf 'true.\n'; sleep 0.3; printf 'halt.\n'; } | ` +
|
|
1538
1632
|
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
1539
1633
|
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
1540
1634
|
cwd: packageRoot,
|
|
@@ -4338,6 +4432,7 @@ function runCli(args, options = {}) {
|
|
|
4338
4432
|
encoding: 'utf8',
|
|
4339
4433
|
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
4340
4434
|
input: options.input ?? undefined,
|
|
4435
|
+
timeout: options.timeout ?? undefined,
|
|
4341
4436
|
});
|
|
4342
4437
|
}
|
|
4343
4438
|
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5253,10 +5253,12 @@ write_event(Path, Event) :-
|
|
|
5253
5253
|
|
|
5254
5254
|
The period is essential when another Prolog processor will read the result as
|
|
5255
5255
|
a term. `write/1-2` uses readable conventional syntax, `writeq/1-2` quotes
|
|
5256
|
-
where needed, and `write_canonical/1-2` exposes canonical structure.
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5256
|
+
where needed, and `write_canonical/1-2` exposes canonical structure. Dotted
|
|
5257
|
+
graphic atoms do not need quotes merely because they contain a period:
|
|
5258
|
+
`writeq(./*)`, `writeq(.*)`, and `writeq(...*)` output `./*`, `.*`, and `...*`
|
|
5259
|
+
respectively. ISO term output uses only the separator characters needed by the
|
|
5260
|
+
syntax, so functional arguments and list elements are emitted compactly; for
|
|
5261
|
+
example `writeq([a,b])` outputs `[a,b]`.
|
|
5260
5262
|
`write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
|
|
5261
5263
|
`variable_names/1`.
|
|
5262
5264
|
|
|
@@ -5377,10 +5379,12 @@ quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
|
|
|
5377
5379
|
are not syntax.
|
|
5378
5380
|
|
|
5379
5381
|
A `/*` sequence opens a block comment only when it begins a token; inside a
|
|
5380
|
-
maximal graphic token the slash and star remain atom characters.
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5382
|
+
maximal graphic token the slash and star remain atom characters. Graphic tokens
|
|
5383
|
+
are formed maximally before a period can be recognized as the terminating full
|
|
5384
|
+
stop. Consequently, interactive input `*.` or `./*.` is not yet a complete term:
|
|
5385
|
+
the period is part of the graphic atom and the reader waits for a separate
|
|
5386
|
+
terminating full stop. Thus `./*. .` reads the atom `./*.` and consumes the
|
|
5387
|
+
second period as the terminator.
|
|
5384
5388
|
|
|
5385
5389
|
In the grammar below, `{ x }` means zero or more repetitions of `x`, `[ x ]`
|
|
5386
5390
|
means that `x` is optional, and parentheses group alternatives. These marks
|
|
@@ -6344,8 +6348,10 @@ period-terminated query with no solutions prints `false.`; a solution without
|
|
|
6344
6348
|
visible variable bindings prints `true.`. Answer substitutions are rendered as
|
|
6345
6349
|
valid Prolog syntax under the current operator table: when a bound value would
|
|
6346
6350
|
not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
|
|
6347
|
-
for example `T = (a = b).` rather than the invalid `T = a = b.`.
|
|
6348
|
-
|
|
6351
|
+
for example `T = (a = b).` rather than the invalid `T = a = b.`. When an answer
|
|
6352
|
+
ends in a graphic token, the top level inserts layout before its terminating
|
|
6353
|
+
full stop so the two tokens cannot merge; for example `?- X = .* .` displays
|
|
6354
|
+
`X = .* .`, not `X = .*.`. Use `[file].` or `['file.pl'].` to
|
|
6349
6355
|
consult local source, and `halt.` or `halt(Status).` to leave the top level.
|
|
6350
6356
|
When `read/1-2` or `read_term/2-3` actually reaches interactive
|
|
6351
6357
|
`user_input`, the top level requests the next full-stop-terminated Prolog term
|