eyeprolog 1.2.40 → 1.2.42
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/cli.js +5 -4
- package/src/iso.js +61 -35
- package/src/parser.js +0 -4
- package/src/platform.js +35 -5
- 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 +106 -16
- package/the-art-of-eyeprolog.md +21 -11
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { goalsFromSource } from './goal-metadata.js';
|
|
7
|
+
import { memoryStatistics } from './platform.js';
|
|
7
8
|
|
|
8
9
|
let engineModule = null;
|
|
9
10
|
let explanationModule = null;
|
|
@@ -230,9 +231,9 @@ async function runDefault(engine, program, options) {
|
|
|
230
231
|
} catch (error) {
|
|
231
232
|
if (error?.name !== 'HaltSignal') throw error;
|
|
232
233
|
process.exitCode = error.code;
|
|
234
|
+
} finally {
|
|
235
|
+
if (options.stats) printStats(solver.stats);
|
|
233
236
|
}
|
|
234
|
-
|
|
235
|
-
if (options.stats) printStats(solver.stats);
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
function writeExplanation(explanation, program, resolved, registry) {
|
|
@@ -259,7 +260,7 @@ Options:
|
|
|
259
260
|
-h, --help Show this help text and exit.
|
|
260
261
|
-p, --proof Enable proof explanations.
|
|
261
262
|
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
262
|
-
-s, --stats Print solver statistics to stderr after execution.
|
|
263
|
+
-s, --stats Print solver and memory statistics to stderr after execution.
|
|
263
264
|
--iso-strict Use ISO/IEC 13211-1 core + Corrigenda 1-3 only;
|
|
264
265
|
reject EyeProlog language extensions and disable automatic tabling.
|
|
265
266
|
-v, --version Show the package version and exit.
|
|
@@ -294,7 +295,7 @@ function printWarnings(program) {
|
|
|
294
295
|
|
|
295
296
|
function printStats(stats) {
|
|
296
297
|
process.stderr.write('eyeprolog stats:\n');
|
|
297
|
-
for (const [key, value] of Object.entries(stats)) {
|
|
298
|
+
for (const [key, value] of Object.entries({ ...stats, ...memoryStatistics() })) {
|
|
298
299
|
process.stderr.write(` ${key}: ${value}\n`);
|
|
299
300
|
}
|
|
300
301
|
}
|
package/src/iso.js
CHANGED
|
@@ -1074,9 +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);
|
|
1079
|
-
const
|
|
1086
|
+
const convert = activeCharConverter(solver);
|
|
1080
1087
|
let quote = null, lineComment = false, blockComment = false;
|
|
1081
1088
|
for (let i = stream.position; i < source.length; i++) {
|
|
1082
1089
|
const ch = source[i], next = source[i + 1];
|
|
@@ -1106,12 +1113,7 @@ function* termTextCandidates(stream) {
|
|
|
1106
1113
|
continue;
|
|
1107
1114
|
}
|
|
1108
1115
|
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
1109
|
-
if (
|
|
1110
|
-
// A buffered stream exposes what follows the layout after this dot. If
|
|
1111
|
-
// the dot continues a graphic token and later non-layout input exists,
|
|
1112
|
-
// it is part of that maximal token rather than an early end char. The
|
|
1113
|
-
// interactive reader makes the corresponding decision incrementally.
|
|
1114
|
-
if (continuesGraphicToken(source, i) && lastBufferedNonLayout > i) continue;
|
|
1116
|
+
if (isTerminatingFullStop(source, i, convert)) {
|
|
1115
1117
|
yield { text: source.slice(stream.position, i + 1), end: i + 1 };
|
|
1116
1118
|
}
|
|
1117
1119
|
}
|
|
@@ -1223,7 +1225,7 @@ function readTermFromStream(stream, solver) {
|
|
|
1223
1225
|
let requestedInteractiveTerm = false;
|
|
1224
1226
|
while (true) {
|
|
1225
1227
|
let sawCandidate = false;
|
|
1226
|
-
for (const candidate of termTextCandidates(stream)) {
|
|
1228
|
+
for (const candidate of termTextCandidates(stream, solver)) {
|
|
1227
1229
|
sawCandidate = true;
|
|
1228
1230
|
if (candidate.lexicalError) {
|
|
1229
1231
|
stream.position = candidate.end;
|
|
@@ -1713,21 +1715,22 @@ function numberListBuiltin(kind) {
|
|
|
1713
1715
|
const list = deref(goal.args[1], env);
|
|
1714
1716
|
if (value.type === VAR && list.type === VAR) throw new PrologError('instantiation_error');
|
|
1715
1717
|
const text = numberListText(list, env, kind, value.type === NUMBER);
|
|
1716
|
-
const next = env.clone();
|
|
1717
1718
|
if (value.type === NUMBER) {
|
|
1718
1719
|
if (text != null) {
|
|
1719
1720
|
const parsed = parseIsoNumber(text);
|
|
1720
1721
|
if (parsed == null) throw numberSyntaxError;
|
|
1721
|
-
if (sameNumber(value, parsed)) yield
|
|
1722
|
+
if (sameNumber(value, parsed)) yield env.clone();
|
|
1722
1723
|
return;
|
|
1723
1724
|
}
|
|
1724
1725
|
const items = characters(canonicalNumberText(value)).map((ch) =>
|
|
1725
1726
|
kind === 'chars' ? atom(ch) : numberTerm(ch.codePointAt(0)));
|
|
1727
|
+
const next = env.clone();
|
|
1726
1728
|
if (unify(goal.args[1], listFromItems(items), next)) yield next;
|
|
1727
1729
|
return;
|
|
1728
1730
|
}
|
|
1729
1731
|
const parsed = parseIsoNumber(text);
|
|
1730
1732
|
if (parsed == null) throw numberSyntaxError;
|
|
1733
|
+
const next = env.clone();
|
|
1731
1734
|
if (unify(goal.args[0], parsed, next)) yield next;
|
|
1732
1735
|
};
|
|
1733
1736
|
}
|
|
@@ -1977,36 +1980,59 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
1977
1980
|
solver.absorbStatsFrom(child);
|
|
1978
1981
|
}
|
|
1979
1982
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
const
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
start = i + 1;
|
|
1998
|
-
}
|
|
1983
|
+
const defaultErrorContext = atom('eyeprolog');
|
|
1984
|
+
|
|
1985
|
+
function parseFormalErrorTerm(text) {
|
|
1986
|
+
const open = text.indexOf('(');
|
|
1987
|
+
if (open === -1) return atom(text);
|
|
1988
|
+
const name = text.slice(0, open);
|
|
1989
|
+
const inner = text.slice(open + 1, -1);
|
|
1990
|
+
const args = [];
|
|
1991
|
+
let start = 0;
|
|
1992
|
+
let depth = 0;
|
|
1993
|
+
for (let i = 0; i <= inner.length; i++) {
|
|
1994
|
+
const ch = inner[i];
|
|
1995
|
+
if (ch === '(') depth++;
|
|
1996
|
+
else if (ch === ')') depth--;
|
|
1997
|
+
else if ((ch === ',' || i === inner.length) && depth === 0) {
|
|
1998
|
+
args.push(parseFormalErrorTerm(inner.slice(start, i).trim()));
|
|
1999
|
+
start = i + 1;
|
|
1999
2000
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2001
|
+
}
|
|
2002
|
+
return compound(name, args);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function hasDefaultGroundErrorShape(error) {
|
|
2006
|
+
return error.formalTerm == null && error.culprit == null && error.contextTerm == null;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
export function formalErrorTerm(error) {
|
|
2010
|
+
// Reused processor errors can occur hundreds of thousands of times in a
|
|
2011
|
+
// caught failure loop. Cache their immutable ground error/2 term on the
|
|
2012
|
+
// error object itself instead of rebuilding the same tree on every catch.
|
|
2013
|
+
if (hasDefaultGroundErrorShape(error) && error._groundErrorTerm != null) {
|
|
2014
|
+
return error._groundErrorTerm;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const context = error.contextTerm ?? defaultErrorContext;
|
|
2018
|
+
let formal = error.formalTerm ?? parseFormalErrorTerm(error.formal);
|
|
2003
2019
|
if (error.culprit != null) {
|
|
2004
2020
|
if (formal.type === COMPOUND) formal = compound(formal.name, [...formal.args, error.culprit]);
|
|
2005
2021
|
else if (formal.type === ATOM && formal.name === 'uninstantiation_error') {
|
|
2006
2022
|
formal = compound(formal.name, [error.culprit]);
|
|
2007
2023
|
}
|
|
2008
2024
|
}
|
|
2009
|
-
|
|
2025
|
+
const term = compound('error', [formal, context]);
|
|
2026
|
+
if (hasDefaultGroundErrorShape(error)) error._groundErrorTerm = term;
|
|
2027
|
+
return term;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function prologErrorBall(error) {
|
|
2031
|
+
const term = formalErrorTerm(error);
|
|
2032
|
+
// Ground terms are immutable from unification's perspective: bindings are
|
|
2033
|
+
// recorded only in the catcher Env, so copying them is unnecessary.
|
|
2034
|
+
if (hasDefaultGroundErrorShape(error) || termIsGround(term)) return term;
|
|
2035
|
+
return freshCopy(term, new Env());
|
|
2010
2036
|
}
|
|
2011
2037
|
function* catchBuiltin({ solver, goal, env }) {
|
|
2012
2038
|
const child = solver.cloneForInnerGoal();
|
|
@@ -2018,7 +2044,7 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2018
2044
|
const ball = error instanceof ThrownTerm
|
|
2019
2045
|
? error.term
|
|
2020
2046
|
: error instanceof PrologError
|
|
2021
|
-
?
|
|
2047
|
+
? prologErrorBall(error)
|
|
2022
2048
|
: null;
|
|
2023
2049
|
if (ball == null) throw error;
|
|
2024
2050
|
const recovered = env.clone();
|
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/platform.js
CHANGED
|
@@ -23,6 +23,13 @@ export function currentWorkingDirectory() {
|
|
|
23
23
|
return isNode && typeof process.cwd === 'function' ? process.cwd() : '/';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function oldGenerationUsedSize() {
|
|
27
|
+
if (!isNode || typeof v8?.getHeapSpaceStatistics !== 'function') return null;
|
|
28
|
+
return v8.getHeapSpaceStatistics()
|
|
29
|
+
.filter(({ space_name: name }) => name !== 'read_only_space' && !name.startsWith('new_'))
|
|
30
|
+
.reduce((total, { space_used_size: size }) => total + size, 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export function usedHeapSize() {
|
|
27
34
|
if (isNode && typeof process.memoryUsage === 'function') {
|
|
28
35
|
// --max-old-space-size constrains V8's old generation, not the complete
|
|
@@ -30,11 +37,9 @@ export function usedHeapSize() {
|
|
|
30
37
|
// with total heap use makes bursts of collectible new-space objects look
|
|
31
38
|
// like retained memory. Measure the corresponding non-young spaces when
|
|
32
39
|
// an old-space limit was supplied.
|
|
33
|
-
if (configuredOldSpaceLimit != null
|
|
34
|
-
const
|
|
35
|
-
return
|
|
36
|
-
.filter(({ space_name: name }) => name !== 'read_only_space' && !name.startsWith('new_'))
|
|
37
|
-
.reduce((total, { space_used_size: size }) => total + size, 0);
|
|
40
|
+
if (configuredOldSpaceLimit != null) {
|
|
41
|
+
const oldGeneration = oldGenerationUsedSize();
|
|
42
|
+
if (oldGeneration != null) return oldGeneration;
|
|
38
43
|
}
|
|
39
44
|
return process.memoryUsage().heapUsed;
|
|
40
45
|
}
|
|
@@ -42,6 +47,31 @@ export function usedHeapSize() {
|
|
|
42
47
|
return Number.isFinite(memory?.usedJSHeapSize) ? memory.usedJSHeapSize : null;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
export function memoryStatistics() {
|
|
51
|
+
const stats = {};
|
|
52
|
+
if (isNode && typeof process.memoryUsage === 'function') {
|
|
53
|
+
const memory = process.memoryUsage();
|
|
54
|
+
stats.memory_heap_used_bytes = memory.heapUsed;
|
|
55
|
+
const oldGeneration = oldGenerationUsedSize();
|
|
56
|
+
if (oldGeneration != null) stats.memory_old_generation_used_bytes = oldGeneration;
|
|
57
|
+
stats.memory_guard_used_bytes = configuredOldSpaceLimit != null && oldGeneration != null
|
|
58
|
+
? oldGeneration
|
|
59
|
+
: memory.heapUsed;
|
|
60
|
+
stats.memory_rss_bytes = memory.rss;
|
|
61
|
+
} else {
|
|
62
|
+
const memory = globalThis.performance?.memory;
|
|
63
|
+
if (Number.isFinite(memory?.usedJSHeapSize)) {
|
|
64
|
+
stats.memory_heap_used_bytes = memory.usedJSHeapSize;
|
|
65
|
+
stats.memory_guard_used_bytes = memory.usedJSHeapSize;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const softLimit = softHeapLimit();
|
|
69
|
+
const hardLimit = hardHeapLimit();
|
|
70
|
+
if (Number.isFinite(softLimit)) stats.memory_soft_limit_bytes = softLimit;
|
|
71
|
+
if (Number.isFinite(hardLimit)) stats.memory_hard_limit_bytes = hardLimit;
|
|
72
|
+
return stats;
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
export function softHeapLimit() {
|
|
46
76
|
const limit = hardHeapLimit();
|
|
47
77
|
// Leave ample room for the generator stack to unwind and for the top level
|
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
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
parseProgramText,
|
|
40
40
|
} from '../src/index.js';
|
|
41
41
|
import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText } from '../src/parser.js';
|
|
42
|
+
import { PrologError, formalErrorTerm } from '../src/iso.js';
|
|
42
43
|
import { compareTerms } from '../src/term.js';
|
|
43
44
|
import { formatTermForWrite } from '../src/write.js';
|
|
44
45
|
import { selectClauseCandidates } from '../src/program.js';
|
|
@@ -102,6 +103,16 @@ export function runRegression(reporter = new TestReporter()) {
|
|
|
102
103
|
|
|
103
104
|
function regressionCases() {
|
|
104
105
|
return [
|
|
106
|
+
{
|
|
107
|
+
name: 'large source scanning avoids quadratic full-stop lookback',
|
|
108
|
+
run: () => {
|
|
109
|
+
const result = runCli(['examples/path-discovery.pl'], { timeout: 10000 });
|
|
110
|
+
if (result.error) throw new Error(`path-discovery timed out or failed to launch: ${result.error.message}`);
|
|
111
|
+
assertEqual(result.status, 0, `path-discovery status; stderr=${result.stderr}`);
|
|
112
|
+
assertIncludes(result.stdout, "airroute('Ostend-Bruges International Airport', 'Václav Havel Airport Prague'",
|
|
113
|
+
'path-discovery result');
|
|
114
|
+
},
|
|
115
|
+
},
|
|
105
116
|
{
|
|
106
117
|
name: '--proof rule fact explanation output',
|
|
107
118
|
run: () => runWhy({
|
|
@@ -733,24 +744,77 @@ c4 ?- call((!;1)).
|
|
|
733
744
|
assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
|
|
734
745
|
},
|
|
735
746
|
},
|
|
747
|
+
{
|
|
748
|
+
name: 'top level separates terminal full stop from graphic answers (issue #44)',
|
|
749
|
+
run: () => {
|
|
750
|
+
const result = runCli([], { input: 'X = .* .\nhalt.\n' });
|
|
751
|
+
assertEqual(result.status, 0, 'issue #44 exit status');
|
|
752
|
+
assertIncludes(result.stdout, 'X = .* .\n', 'graphic binding is separated from terminal full stop');
|
|
753
|
+
assertNotIncludes(result.stdout, 'X = .*.\n', 'terminal full stop is not absorbed into graphic atom');
|
|
754
|
+
assertEqual(result.stderr, '', 'issue #44 stderr');
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: 'writeq leaves ISO dotted graphic atoms unquoted (WG17 #371-373)',
|
|
759
|
+
run: () => {
|
|
760
|
+
const result = runCli([], {
|
|
761
|
+
input: 'writeq(./*).\nwriteq(.*).\nwriteq(...*).\nhalt.\n',
|
|
762
|
+
});
|
|
763
|
+
assertEqual(result.status, 0, 'dotted graphic writeq exit status');
|
|
764
|
+
assertIncludes(result.stdout, ' ./* true.\n', 'writeq ./* is unquoted');
|
|
765
|
+
assertIncludes(result.stdout, ' .* true.\n', 'writeq .* is unquoted');
|
|
766
|
+
assertIncludes(result.stdout, ' ...* true.\n', 'writeq ...* is unquoted');
|
|
767
|
+
assertNotIncludes(result.stdout, "'./*'", 'writeq ./* has no quotes');
|
|
768
|
+
assertNotIncludes(result.stdout, "'.*'", 'writeq .* has no quotes');
|
|
769
|
+
assertNotIncludes(result.stdout, "'...*'", 'writeq ...* has no quotes');
|
|
770
|
+
},
|
|
771
|
+
},
|
|
736
772
|
{
|
|
737
773
|
name: 'readers distinguish graphic tokens, comments, and full stops (issue #41)',
|
|
738
774
|
run: () => {
|
|
739
|
-
const parsed = parseProgramText('
|
|
775
|
+
const parsed = parseProgramText('./* .');
|
|
740
776
|
assertEqual(parsed.length, 1, 'graphic atom clause count');
|
|
741
777
|
assertEqual(parsed[0].head.name, './*', 'comment opener stays inside graphic atom');
|
|
742
778
|
|
|
779
|
+
// A dot immediately after a graphic token belongs to that maximal
|
|
780
|
+
// token. A separate end char is therefore required even at a line
|
|
781
|
+
// boundary; this is the waiting behavior called out in WG17 #370-373.
|
|
782
|
+
const waitProgram = Program.parse('');
|
|
783
|
+
const waitSolver = new Solver(waitProgram, {
|
|
784
|
+
registry: getEyePrologRegistry(),
|
|
785
|
+
ioOptions: { input: '*.\n' },
|
|
786
|
+
});
|
|
787
|
+
const waitStream = waitSolver.io.resolve('user_input');
|
|
788
|
+
let refillRequests = 0;
|
|
789
|
+
waitStream.interactiveReadTerm = () => {
|
|
790
|
+
refillRequests++;
|
|
791
|
+
return '.\n';
|
|
792
|
+
};
|
|
793
|
+
const waitGoal = parseGoalText('read(T)', {
|
|
794
|
+
operatorDefinitions: [...waitProgram.operators.values()],
|
|
795
|
+
});
|
|
796
|
+
const waitAnswers = [...waitSolver.solve([waitGoal], new Env(), 0)];
|
|
797
|
+
assertEqual(waitAnswers.length, 1, 'graphic token read answer after refill');
|
|
798
|
+
assertEqual(refillRequests, 1, 'graphic token boundary waits for a separate end char');
|
|
799
|
+
assertEqual(copyResolved(waitGoal.args[0], waitAnswers[0]).name, '*.', 'maximal graphic token after refill');
|
|
800
|
+
|
|
743
801
|
const read = runEyeProlog('', {
|
|
744
802
|
goal: 'read(T)',
|
|
745
|
-
ioOptions: { input: './*.' },
|
|
803
|
+
ioOptions: { input: './*. .' },
|
|
746
804
|
});
|
|
747
|
-
assertEqual(read.stdout, "read(
|
|
805
|
+
assertEqual(read.stdout, "read(./*.).\n", 'dotted graphic atom writeq readback');
|
|
806
|
+
|
|
807
|
+
const ellipsisGraphic = runEyeProlog('', {
|
|
808
|
+
goal: 'read(T)',
|
|
809
|
+
ioOptions: { input: '...*\n.\n' },
|
|
810
|
+
});
|
|
811
|
+
assertEqual(ellipsisGraphic.stdout, "read(...*).\n", 'ellipsis prefix remains inside maximal graphic atom');
|
|
748
812
|
|
|
749
813
|
const consecutive = runEyeProlog('answer(A, B) :- read(A), read(B).\n', {
|
|
750
814
|
goal: 'answer(A, B)',
|
|
751
815
|
ioOptions: { input: './*. .\nok.\n' },
|
|
752
816
|
});
|
|
753
|
-
assertEqual(consecutive.stdout, "answer(
|
|
817
|
+
assertEqual(consecutive.stdout, "answer(./*., ok).\n", 'following read starts after the complete term');
|
|
754
818
|
|
|
755
819
|
// A possible full stop can fail to complete the term while still
|
|
756
820
|
// extending a current graphic operator into an ordinary atom. The
|
|
@@ -820,7 +884,7 @@ c4 ?- call((!;1)).
|
|
|
820
884
|
input: 'read(T).\n./*. .\nread(T).\nok.\nread(T).\n!.!.\nhalt.\n',
|
|
821
885
|
});
|
|
822
886
|
assertEqual(repl.status, 0, 'REPL exit status');
|
|
823
|
-
assertIncludes(repl.stdout, 'T =
|
|
887
|
+
assertIncludes(repl.stdout, 'T = ./*. .', 'REPL dotted graphic atom answer');
|
|
824
888
|
assertNotIncludes(repl.stdout, "T = './*.'", 'REPL dotted graphic atom has no spurious quotes');
|
|
825
889
|
assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
|
|
826
890
|
assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
|
|
@@ -1562,10 +1626,10 @@ c4 ?- call((!;1)).
|
|
|
1562
1626
|
if (available.status !== 0) return;
|
|
1563
1627
|
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
1564
1628
|
const scriptCommand =
|
|
1565
|
-
`{ printf 'read(X), read(Y).\n'; sleep 0.
|
|
1566
|
-
`printf 'foo.\n'; sleep 0.
|
|
1567
|
-
`printf 'read(Z).\n'; sleep 0.
|
|
1568
|
-
`printf 'true.\n'; sleep 0.
|
|
1629
|
+
`{ printf 'read(X), read(Y).\n'; sleep 0.3; ` +
|
|
1630
|
+
`printf 'foo.\n'; sleep 0.3; printf 'bar.\n'; sleep 0.3; ` +
|
|
1631
|
+
`printf 'read(Z).\n'; sleep 0.3; printf '\\004'; sleep 0.3; ` +
|
|
1632
|
+
`printf 'true.\n'; sleep 0.3; printf 'halt.\n'; } | ` +
|
|
1569
1633
|
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
1570
1634
|
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
1571
1635
|
cwd: packageRoot,
|
|
@@ -1876,13 +1940,29 @@ c4 ?- call((!;1)).
|
|
|
1876
1940
|
|
|
1877
1941
|
|
|
1878
1942
|
{
|
|
1879
|
-
name: '--stats prints solver statistics to stderr',
|
|
1943
|
+
name: '--stats prints solver and memory statistics to stderr',
|
|
1880
1944
|
run: () => {
|
|
1881
1945
|
const result = runCli(['--stats', '-'], { input: '%% goal: q(X, Y)\np(a, b).\nq(X, Y) :- p(X, Y).\n' });
|
|
1882
1946
|
assertEqual(result.status, 0, 'exit status');
|
|
1883
1947
|
assertEqual(result.stdout, 'q(a, b).\n', 'stdout');
|
|
1884
1948
|
assertIncludes(result.stderr, 'eyeprolog stats:\n', 'stderr');
|
|
1885
1949
|
assertIncludes(result.stderr, ' solve_goals_calls:', 'stderr');
|
|
1950
|
+
assertIncludes(result.stderr, ' memory_heap_used_bytes:', 'stderr');
|
|
1951
|
+
assertIncludes(result.stderr, ' memory_old_generation_used_bytes:', 'stderr');
|
|
1952
|
+
assertIncludes(result.stderr, ' memory_guard_used_bytes:', 'stderr');
|
|
1953
|
+
assertIncludes(result.stderr, ' memory_rss_bytes:', 'stderr');
|
|
1954
|
+
assertIncludes(result.stderr, ' memory_soft_limit_bytes:', 'stderr');
|
|
1955
|
+
assertIncludes(result.stderr, ' memory_hard_limit_bytes:', 'stderr');
|
|
1956
|
+
},
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
name: '--stats is still printed when a query raises an error',
|
|
1960
|
+
run: () => {
|
|
1961
|
+
const result = runCli(['--stats', '-'], { input: "%% goal: number_chars(N, ['x'])\n" });
|
|
1962
|
+
assertEqual(result.status, 1, 'exit status');
|
|
1963
|
+
assertIncludes(result.stderr, 'eyeprolog stats:\n', 'stderr');
|
|
1964
|
+
assertIncludes(result.stderr, ' memory_heap_used_bytes:', 'stderr');
|
|
1965
|
+
assertIncludes(result.stderr, 'eyeprolog: error(syntax_error(number))', 'stderr');
|
|
1886
1966
|
},
|
|
1887
1967
|
},
|
|
1888
1968
|
{
|
|
@@ -2896,6 +2976,15 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2896
2976
|
assertEqual(result.stdout, '300000', 'fresh-variable answer count');
|
|
2897
2977
|
},
|
|
2898
2978
|
},
|
|
2979
|
+
{
|
|
2980
|
+
name: 'ground Prolog error terms are reused across catches',
|
|
2981
|
+
run: () => {
|
|
2982
|
+
const error = new PrologError('syntax_error(number)');
|
|
2983
|
+
const first = formalErrorTerm(error);
|
|
2984
|
+
const second = formalErrorTerm(error);
|
|
2985
|
+
assertEqual(first === second, true, 'reused ground error term');
|
|
2986
|
+
},
|
|
2987
|
+
},
|
|
2899
2988
|
{
|
|
2900
2989
|
name: 'caught number syntax errors do not exhaust memory on distinct inputs',
|
|
2901
2990
|
run: () => {
|
|
@@ -2916,22 +3005,22 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2916
3005
|
const goal = parseGoalText('trial(Chars)');
|
|
2917
3006
|
let count = 0;
|
|
2918
3007
|
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2919
|
-
if (++count ===
|
|
3008
|
+
if (++count === 250000) break;
|
|
2920
3009
|
}
|
|
2921
|
-
if (count !==
|
|
3010
|
+
if (count !== 250000) throw new Error('unexpected answer count: ' + count);
|
|
2922
3011
|
process.stdout.write(String(count));
|
|
2923
3012
|
`;
|
|
2924
3013
|
const result = spawnSync(process.execPath, [
|
|
2925
|
-
//
|
|
2926
|
-
//
|
|
3014
|
+
// Run well past the roughly 126,000-answer failure reported in #28
|
|
3015
|
+
// while keeping the host heap deliberately constrained.
|
|
2927
3016
|
'--max-old-space-size=32',
|
|
2928
3017
|
'--input-type=module',
|
|
2929
3018
|
'--eval',
|
|
2930
3019
|
script,
|
|
2931
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout:
|
|
3020
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2932
3021
|
if (result.error) throw result.error;
|
|
2933
3022
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2934
|
-
assertEqual(result.stdout, '
|
|
3023
|
+
assertEqual(result.stdout, '250000', 'distinct number syntax attempts');
|
|
2935
3024
|
},
|
|
2936
3025
|
},
|
|
2937
3026
|
{
|
|
@@ -4369,6 +4458,7 @@ function runCli(args, options = {}) {
|
|
|
4369
4458
|
encoding: 'utf8',
|
|
4370
4459
|
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
4371
4460
|
input: options.input ?? undefined,
|
|
4461
|
+
timeout: options.timeout ?? undefined,
|
|
4372
4462
|
});
|
|
4373
4463
|
}
|
|
4374
4464
|
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -4633,7 +4633,11 @@ testing is a powerful guard during program transformation.
|
|
|
4633
4633
|
`--stats` reports work, not meaning. A high solution count may be necessary or
|
|
4634
4634
|
may indicate a generator that should be constrained. Many table hits may show
|
|
4635
4635
|
effective reuse; many distinct table entries may reveal an argument that
|
|
4636
|
-
prevents calls from sharing.
|
|
4636
|
+
prevents calls from sharing. On the Node CLI it also reports current heap use,
|
|
4637
|
+
non-young/old-generation use, the amount currently compared with the memory
|
|
4638
|
+
guard, resident-set size, and the soft and hard memory ceilings in bytes. These memory figures are printed even when execution ends by
|
|
4639
|
+
raising a Prolog error, which makes `--stats` useful when reproducing bounded
|
|
4640
|
+
memory failures such as long-running conversion stress tests.
|
|
4637
4641
|
|
|
4638
4642
|
Compare statistics only between runs with the same query, data, and observable
|
|
4639
4643
|
answer contract. A faster program that silently loses answers is not an
|
|
@@ -5253,10 +5257,12 @@ write_event(Path, Event) :-
|
|
|
5253
5257
|
|
|
5254
5258
|
The period is essential when another Prolog processor will read the result as
|
|
5255
5259
|
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
|
-
|
|
5260
|
+
where needed, and `write_canonical/1-2` exposes canonical structure. Dotted
|
|
5261
|
+
graphic atoms do not need quotes merely because they contain a period:
|
|
5262
|
+
`writeq(./*)`, `writeq(.*)`, and `writeq(...*)` output `./*`, `.*`, and `...*`
|
|
5263
|
+
respectively. ISO term output uses only the separator characters needed by the
|
|
5264
|
+
syntax, so functional arguments and list elements are emitted compactly; for
|
|
5265
|
+
example `writeq([a,b])` outputs `[a,b]`.
|
|
5260
5266
|
`write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
|
|
5261
5267
|
`variable_names/1`.
|
|
5262
5268
|
|
|
@@ -5377,10 +5383,12 @@ quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
|
|
|
5377
5383
|
are not syntax.
|
|
5378
5384
|
|
|
5379
5385
|
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
|
-
|
|
5386
|
+
maximal graphic token the slash and star remain atom characters. Graphic tokens
|
|
5387
|
+
are formed maximally before a period can be recognized as the terminating full
|
|
5388
|
+
stop. Consequently, interactive input `*.` or `./*.` is not yet a complete term:
|
|
5389
|
+
the period is part of the graphic atom and the reader waits for a separate
|
|
5390
|
+
terminating full stop. Thus `./*. .` reads the atom `./*.` and consumes the
|
|
5391
|
+
second period as the terminator.
|
|
5384
5392
|
|
|
5385
5393
|
In the grammar below, `{ x }` means zero or more repetitions of `x`, `[ x ]`
|
|
5386
5394
|
means that `x` is optional, and parentheses group alternatives. These marks
|
|
@@ -6344,8 +6352,10 @@ period-terminated query with no solutions prints `false.`; a solution without
|
|
|
6344
6352
|
visible variable bindings prints `true.`. Answer substitutions are rendered as
|
|
6345
6353
|
valid Prolog syntax under the current operator table: when a bound value would
|
|
6346
6354
|
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
|
-
|
|
6355
|
+
for example `T = (a = b).` rather than the invalid `T = a = b.`. When an answer
|
|
6356
|
+
ends in a graphic token, the top level inserts layout before its terminating
|
|
6357
|
+
full stop so the two tokens cannot merge; for example `?- X = .* .` displays
|
|
6358
|
+
`X = .* .`, not `X = .*.`. Use `[file].` or `['file.pl'].` to
|
|
6349
6359
|
consult local source, and `halt.` or `halt(Status).` to leave the top level.
|
|
6350
6360
|
When `read/1-2` or `read_term/2-3` actually reaches interactive
|
|
6351
6361
|
`user_input`, the top level requests the next full-stop-terminated Prolog term
|