eyeprolog 1.2.38 → 1.2.40
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 +46 -12
- package/src/parser.js +27 -8
- package/src/repl.js +20 -7
- package/test/run-regression.mjs +65 -0
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1076,6 +1076,7 @@ function* nlBuiltin({ solver, goal, env }) {
|
|
|
1076
1076
|
|
|
1077
1077
|
function* termTextCandidates(stream) {
|
|
1078
1078
|
const source = String(stream.content);
|
|
1079
|
+
const lastBufferedNonLayout = lastNonLayoutIndex(source, stream.position);
|
|
1079
1080
|
let quote = null, lineComment = false, blockComment = false;
|
|
1080
1081
|
for (let i = stream.position; i < source.length; i++) {
|
|
1081
1082
|
const ch = source[i], next = source[i + 1];
|
|
@@ -1106,14 +1107,29 @@ function* termTextCandidates(stream) {
|
|
|
1106
1107
|
}
|
|
1107
1108
|
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
1108
1109
|
if (ch === '.' && isTerminatingFullStop(source, i)) {
|
|
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;
|
|
1109
1115
|
yield { text: source.slice(stream.position, i + 1), end: i + 1 };
|
|
1110
1116
|
}
|
|
1111
1117
|
}
|
|
1112
1118
|
}
|
|
1113
1119
|
function hasNonLayoutRemainder(source, start) {
|
|
1114
|
-
return source
|
|
1115
|
-
|
|
1116
|
-
|
|
1120
|
+
return lastNonLayoutIndex(source, start) >= start;
|
|
1121
|
+
}
|
|
1122
|
+
function lastNonLayoutIndex(source, start = 0) {
|
|
1123
|
+
const ignored = /[\u0009-\u000d\u0020]+|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g;
|
|
1124
|
+
ignored.lastIndex = start;
|
|
1125
|
+
let cursor = start;
|
|
1126
|
+
let last = -1;
|
|
1127
|
+
for (let match = ignored.exec(source); match != null; match = ignored.exec(source)) {
|
|
1128
|
+
if (match.index > cursor) last = match.index - 1;
|
|
1129
|
+
cursor = match.index + match[0].length;
|
|
1130
|
+
}
|
|
1131
|
+
if (cursor < source.length) last = source.length - 1;
|
|
1132
|
+
return last;
|
|
1117
1133
|
}
|
|
1118
1134
|
function convertedTermText(text, solver) {
|
|
1119
1135
|
if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
|
|
@@ -1178,6 +1194,31 @@ function scopeReadTerm(term) {
|
|
|
1178
1194
|
return { term: copy(term), variables };
|
|
1179
1195
|
}
|
|
1180
1196
|
|
|
1197
|
+
function parseReadTermText(text, solver) {
|
|
1198
|
+
const converted = convertedTermText(text, solver);
|
|
1199
|
+
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1200
|
+
const clauses = parseClauses(converted, {
|
|
1201
|
+
sourceMetadata: false,
|
|
1202
|
+
operatorState,
|
|
1203
|
+
isoStrict: solver.isoStrict,
|
|
1204
|
+
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
1205
|
+
// The stream scanner supplies one candidate ending at this full stop.
|
|
1206
|
+
// Earlier ambiguous dots must remain available to maximal graphic tokens.
|
|
1207
|
+
readTermEnd: converted.length - 1,
|
|
1208
|
+
});
|
|
1209
|
+
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1210
|
+
return clauses[0].head;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
export function isCompleteReadTermText(text, solver) {
|
|
1214
|
+
try {
|
|
1215
|
+
parseReadTermText(text, solver);
|
|
1216
|
+
return true;
|
|
1217
|
+
} catch (_) {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1181
1222
|
function readTermFromStream(stream, solver) {
|
|
1182
1223
|
let requestedInteractiveTerm = false;
|
|
1183
1224
|
while (true) {
|
|
@@ -1189,16 +1230,9 @@ function readTermFromStream(stream, solver) {
|
|
|
1189
1230
|
throw new PrologError('syntax_error(read_term)');
|
|
1190
1231
|
}
|
|
1191
1232
|
try {
|
|
1192
|
-
const
|
|
1193
|
-
const clauses = parseClauses(convertedTermText(candidate.text, solver), {
|
|
1194
|
-
sourceMetadata: false,
|
|
1195
|
-
operatorState,
|
|
1196
|
-
isoStrict: solver.isoStrict,
|
|
1197
|
-
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
1198
|
-
});
|
|
1199
|
-
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1233
|
+
const term = parseReadTermText(candidate.text, solver);
|
|
1200
1234
|
stream.position = candidate.end;
|
|
1201
|
-
return scopeReadTerm(
|
|
1235
|
+
return scopeReadTerm(term);
|
|
1202
1236
|
} catch (_) {
|
|
1203
1237
|
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1204
1238
|
// terminator. Keep scanning until a complete term parses.
|
package/src/parser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tokenizer and recursive-descent parser for the EyeProlog source language.
|
|
2
2
|
// It preserves the compact Prolog-like syntax while producing Term objects for the solver.
|
|
3
3
|
import { ATOM, COMPOUND, atom, compound, cons, emptyList, numberTerm, variable } from './term.js';
|
|
4
|
-
import { isTerminatingFullStop } from './syntax-scan.js';
|
|
4
|
+
import { continuesGraphicToken, isTerminatingFullStop } from './syntax-scan.js';
|
|
5
5
|
|
|
6
6
|
const TOK = {
|
|
7
7
|
EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
|
|
@@ -196,9 +196,25 @@ class Parser {
|
|
|
196
196
|
this.infixOperators = operatorState.infixOperators;
|
|
197
197
|
this.prefixOperators = operatorState.prefixOperators;
|
|
198
198
|
this.postfixOperators = operatorState.postfixOperators;
|
|
199
|
+
this.readTermEnd = Number.isInteger(options.readTermEnd) ? options.readTermEnd : null;
|
|
199
200
|
this.previousToken = null;
|
|
200
201
|
this.token = this.nextToken();
|
|
201
202
|
}
|
|
203
|
+
terminatingFullStop(index = this.pos) {
|
|
204
|
+
// read/1 tries each possible full stop in turn. While parsing a later
|
|
205
|
+
// candidate, a preceding dot after a graphic character belongs to that
|
|
206
|
+
// maximal graphic token; the candidate's final dot is the end char. Keep
|
|
207
|
+
// ordinary program parsing context-free by enabling this only for a
|
|
208
|
+
// designated read-term candidate.
|
|
209
|
+
if (this.readTermEnd != null) {
|
|
210
|
+
if (index === this.readTermEnd) return true;
|
|
211
|
+
if (index < this.readTermEnd && continuesGraphicToken(this.source, index)) {
|
|
212
|
+
const next = this.source[index + 1] ?? '';
|
|
213
|
+
if (next === '%' || /^[\u0009-\u000d\u0020]$/.test(next)) return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return isTerminatingFullStop(this.source, index);
|
|
217
|
+
}
|
|
202
218
|
defineOperator(priority, specifier, name) {
|
|
203
219
|
defineParserOperator(this, priority, specifier, name);
|
|
204
220
|
}
|
|
@@ -366,15 +382,16 @@ class Parser {
|
|
|
366
382
|
this.pos += 3;
|
|
367
383
|
return { type: TOK.ATOM, text: '...', line };
|
|
368
384
|
}
|
|
369
|
-
if (ch === '?' && this.peek(1) === '-'
|
|
385
|
+
if (ch === '?' && this.peek(1) === '-' &&
|
|
386
|
+
!(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
|
|
370
387
|
this.pos += 2;
|
|
371
388
|
return { type: TOK.ATOM, text: '?-', line };
|
|
372
389
|
}
|
|
373
|
-
if (ch === '.' && !
|
|
390
|
+
if (ch === '.' && !this.terminatingFullStop()) {
|
|
374
391
|
const start = this.pos;
|
|
375
392
|
this.take();
|
|
376
393
|
while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
|
|
377
|
-
!
|
|
394
|
+
!this.terminatingFullStop()) this.take();
|
|
378
395
|
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
379
396
|
}
|
|
380
397
|
if (ch === '!') {
|
|
@@ -394,11 +411,13 @@ class Parser {
|
|
|
394
411
|
this.take();
|
|
395
412
|
return { type: punct[ch], text: ch, line, precededByLayout };
|
|
396
413
|
}
|
|
397
|
-
if (ch === ':' && this.peek(1) === '-'
|
|
414
|
+
if (ch === ':' && this.peek(1) === '-' &&
|
|
415
|
+
!(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
|
|
398
416
|
this.pos += 2;
|
|
399
417
|
return { type: TOK.IF, text: ':-', line };
|
|
400
418
|
}
|
|
401
|
-
if (ch === ':'
|
|
419
|
+
if (ch === ':' &&
|
|
420
|
+
!(isGraphicAtomCode(this.peek(1).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 1))) {
|
|
402
421
|
this.take();
|
|
403
422
|
return { type: TOK.ATOM, text: ':', line };
|
|
404
423
|
}
|
|
@@ -544,7 +563,7 @@ class Parser {
|
|
|
544
563
|
const start = this.pos;
|
|
545
564
|
this.take();
|
|
546
565
|
while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
|
|
547
|
-
!
|
|
566
|
+
!this.terminatingFullStop()) this.take();
|
|
548
567
|
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
549
568
|
}
|
|
550
569
|
|
|
@@ -1034,7 +1053,7 @@ export function parseClauses(source, options = {}) {
|
|
|
1034
1053
|
const parserOptions = ownsParserFlagState
|
|
1035
1054
|
? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes } }
|
|
1036
1055
|
: options;
|
|
1037
|
-
if (options.sourceMetadata === false) {
|
|
1056
|
+
if (options.sourceMetadata === false && options.readTermEnd == null) {
|
|
1038
1057
|
const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
|
|
1039
1058
|
if (clauses) return clauses;
|
|
1040
1059
|
if (ownsParserFlagState) parserOptions.parserFlagState.doubleQuotes = initialDoubleQuotes;
|
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 } from './iso.js';
|
|
6
|
+
import { formalErrorTerm, isCompleteReadTermText } from './iso.js';
|
|
7
7
|
import {
|
|
8
8
|
characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
|
|
9
9
|
} from './syntax-scan.js';
|
|
@@ -162,7 +162,7 @@ class LineReader {
|
|
|
162
162
|
return this.terminal && Number.isInteger(this.input.fd);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
readInteractiveTermSync() {
|
|
165
|
+
readInteractiveTermSync(solver = null) {
|
|
166
166
|
if (!this.canReadTermSynchronously()) return null;
|
|
167
167
|
let source = '';
|
|
168
168
|
let prompt = '|: ';
|
|
@@ -172,7 +172,9 @@ class LineReader {
|
|
|
172
172
|
if (line == null) return source.trim() ? source : null;
|
|
173
173
|
source += `${line}\n`;
|
|
174
174
|
const end = terminalFullStop(source);
|
|
175
|
-
if (end >= 0
|
|
175
|
+
if (end >= 0 && acceptsReadTermBoundary(source, end, solver)) {
|
|
176
|
+
return source.slice(0, end + 1) + '\n';
|
|
177
|
+
}
|
|
176
178
|
prompt = '| ';
|
|
177
179
|
}
|
|
178
180
|
}
|
|
@@ -256,7 +258,7 @@ function makeState(engine, sources, output, options = {}, previousState = null,
|
|
|
256
258
|
// let ISO term input request a complete terminal term exactly when read/1-2
|
|
257
259
|
// or read_term/2-3 actually executes. This also works inside conjunctions
|
|
258
260
|
// and user predicates instead of only when read/* is the whole REPL goal.
|
|
259
|
-
userInput.interactiveReadTerm = () => reader.readInteractiveTermSync();
|
|
261
|
+
userInput.interactiveReadTerm = () => reader.readInteractiveTermSync(solver);
|
|
260
262
|
}
|
|
261
263
|
const flagOverrides = new Map(previousState?.flagOverrides ?? []);
|
|
262
264
|
for (const [name, value] of flagOverrides) {
|
|
@@ -300,7 +302,7 @@ async function prepareInteractiveTermInput(state, goal, reader) {
|
|
|
300
302
|
const stream = interactiveTermInputStream(state, goal);
|
|
301
303
|
if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
|
|
302
304
|
|
|
303
|
-
const text = await readInteractiveTerm(reader);
|
|
305
|
+
const text = await readInteractiveTerm(reader, state.solver);
|
|
304
306
|
if (text == null) return;
|
|
305
307
|
stream.content += text;
|
|
306
308
|
stream.pastEnd = false;
|
|
@@ -334,7 +336,7 @@ function explicitInputReference(term) {
|
|
|
334
336
|
return null;
|
|
335
337
|
}
|
|
336
338
|
|
|
337
|
-
async function readInteractiveTerm(reader) {
|
|
339
|
+
async function readInteractiveTerm(reader, solver = null) {
|
|
338
340
|
let source = '';
|
|
339
341
|
let prompt = '|: ';
|
|
340
342
|
while (true) {
|
|
@@ -342,11 +344,22 @@ async function readInteractiveTerm(reader) {
|
|
|
342
344
|
if (line == null) return source.trim() ? source : null;
|
|
343
345
|
source += `${line}\n`;
|
|
344
346
|
const end = terminalFullStop(source);
|
|
345
|
-
if (end >= 0
|
|
347
|
+
if (end >= 0 && acceptsReadTermBoundary(source, end, solver)) {
|
|
348
|
+
return source.slice(0, end + 1) + '\n';
|
|
349
|
+
}
|
|
346
350
|
prompt = '| ';
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
|
|
354
|
+
function acceptsReadTermBoundary(source, end, solver) {
|
|
355
|
+
// A dot after a graphic character has two possible readings. Stop at once
|
|
356
|
+
// when the candidate is already a complete term (for example `./*.`);
|
|
357
|
+
// otherwise keep reading so a later end char can make it part of an atom
|
|
358
|
+
// (for example the first dot in `!,*.\n.`).
|
|
359
|
+
return !continuesGraphicToken(source, end) || solver == null ||
|
|
360
|
+
isCompleteReadTermText(source.slice(0, end + 1), solver);
|
|
361
|
+
}
|
|
362
|
+
|
|
350
363
|
function terminalFullStop(source) {
|
|
351
364
|
let quote = null;
|
|
352
365
|
let lineComment = false;
|
package/test/run-regression.mjs
CHANGED
|
@@ -752,6 +752,62 @@ c4 ?- call((!;1)).
|
|
|
752
752
|
});
|
|
753
753
|
assertEqual(consecutive.stdout, "answer('./*.', ok).\n", 'following read starts after the complete term');
|
|
754
754
|
|
|
755
|
+
// A possible full stop can fail to complete the term while still
|
|
756
|
+
// extending a current graphic operator into an ordinary atom. The
|
|
757
|
+
// later standalone full stop then completes the read term. Exercise
|
|
758
|
+
// every predefined graphic operator, including the tokenizer's
|
|
759
|
+
// special :- and ?- cases, so they cannot diverge from * again.
|
|
760
|
+
const graphicOperator = /^[#$&*+\-./<=>?@^~\\:]+$/;
|
|
761
|
+
const graphicOperators = [...new Set(ISO_OPERATOR_DEFINITIONS.map(([, , name]) => name))]
|
|
762
|
+
.filter((name) => graphicOperator.test(name));
|
|
763
|
+
for (const name of graphicOperators) {
|
|
764
|
+
const program = Program.parse('');
|
|
765
|
+
const solver = new Solver(program, {
|
|
766
|
+
registry: getEyePrologRegistry(),
|
|
767
|
+
ioOptions: { input: `!,${name}.\n.\n` },
|
|
768
|
+
});
|
|
769
|
+
const readGoal = parseGoalText('read(T)', {
|
|
770
|
+
operatorDefinitions: [...program.operators.values()],
|
|
771
|
+
});
|
|
772
|
+
const answers = [...solver.solve([readGoal], new Env(), 0)];
|
|
773
|
+
assertEqual(answers.length, 1, `graphic operator read answer for ${name}`);
|
|
774
|
+
const term = copyResolved(readGoal.args[0], answers[0]);
|
|
775
|
+
assertEqual(term.name, ',', `graphic operator conjunction for ${name}`);
|
|
776
|
+
assertEqual(term.args[0].name, '!', `graphic operator left operand for ${name}`);
|
|
777
|
+
assertEqual(term.args[1].name, `${name}.`, `graphic operator atom for ${name}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Unlike an interactive reader waiting at a line boundary, a buffered
|
|
781
|
+
// stream can see later non-layout input. Its first dot therefore
|
|
782
|
+
// remains in the maximal graphic token, making the adjacent ! invalid
|
|
783
|
+
// rather than prematurely returning the shorter atom.
|
|
784
|
+
for (const name of [...graphicOperators, '?', '#', '@', './*', '//*']) {
|
|
785
|
+
let bufferedError = null;
|
|
786
|
+
try {
|
|
787
|
+
runEyeProlog('', {
|
|
788
|
+
goal: 'read_term(T, [])',
|
|
789
|
+
ioOptions: { input: `${name}.\n!\n.` },
|
|
790
|
+
});
|
|
791
|
+
} catch (caught) {
|
|
792
|
+
bufferedError = caught;
|
|
793
|
+
}
|
|
794
|
+
assertEqual(
|
|
795
|
+
bufferedError?.message,
|
|
796
|
+
'error(syntax_error(read_term))',
|
|
797
|
+
`buffered graphic atom boundary for ${name}`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const bufferedPath = path.join(tmp, 'graphic-atom-boundary.pl');
|
|
802
|
+
fs.writeFileSync(bufferedPath, '*.\n!\n.');
|
|
803
|
+
const namedStream = runEyeProlog([
|
|
804
|
+
`caught(ok) :- open(${sourceAtom(bufferedPath)}, read, S),`,
|
|
805
|
+
' catch(read_term(S, _, []), error(syntax_error(read_term), _), true),',
|
|
806
|
+
' close(S).',
|
|
807
|
+
'',
|
|
808
|
+
].join('\n'), { goal: 'caught(ok)' });
|
|
809
|
+
assertEqual(namedStream.stdout, 'caught(ok).\n', 'named buffered stream syntax error');
|
|
810
|
+
|
|
755
811
|
let error = null;
|
|
756
812
|
try {
|
|
757
813
|
runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
|
|
@@ -769,6 +825,15 @@ c4 ?- call((!;1)).
|
|
|
769
825
|
assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
|
|
770
826
|
assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
|
|
771
827
|
assertEqual(repl.stderr, '', 'REPL stderr');
|
|
828
|
+
|
|
829
|
+
const continuedGraphic = runCli([], {
|
|
830
|
+
input: 'read(T).\n!,*.\n.\nread(T).\na\n.\nhalt.\n',
|
|
831
|
+
});
|
|
832
|
+
assertEqual(continuedGraphic.status, 0, 'continued graphic operator REPL status');
|
|
833
|
+
assertIncludes(continuedGraphic.stdout, 'T = (!, *.).', 'continued graphic operator answer');
|
|
834
|
+
assertIncludes(continuedGraphic.stdout, 'T = a.', 'read after continued graphic operator');
|
|
835
|
+
assertNotIncludes(continuedGraphic.stdout, 'syntax_error', 'continued graphic operator syntax');
|
|
836
|
+
assertEqual(continuedGraphic.stderr, '', 'continued graphic operator REPL stderr');
|
|
772
837
|
},
|
|
773
838
|
},
|
|
774
839
|
{
|