eyeprolog 1.2.38 → 1.2.39
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 +27 -9
- package/src/parser.js +27 -8
- package/src/repl.js +20 -7
- package/test/run-regression.mjs +34 -0
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1178,6 +1178,31 @@ function scopeReadTerm(term) {
|
|
|
1178
1178
|
return { term: copy(term), variables };
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
|
+
function parseReadTermText(text, solver) {
|
|
1182
|
+
const converted = convertedTermText(text, solver);
|
|
1183
|
+
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1184
|
+
const clauses = parseClauses(converted, {
|
|
1185
|
+
sourceMetadata: false,
|
|
1186
|
+
operatorState,
|
|
1187
|
+
isoStrict: solver.isoStrict,
|
|
1188
|
+
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
1189
|
+
// The stream scanner supplies one candidate ending at this full stop.
|
|
1190
|
+
// Earlier ambiguous dots must remain available to maximal graphic tokens.
|
|
1191
|
+
readTermEnd: converted.length - 1,
|
|
1192
|
+
});
|
|
1193
|
+
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1194
|
+
return clauses[0].head;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export function isCompleteReadTermText(text, solver) {
|
|
1198
|
+
try {
|
|
1199
|
+
parseReadTermText(text, solver);
|
|
1200
|
+
return true;
|
|
1201
|
+
} catch (_) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1181
1206
|
function readTermFromStream(stream, solver) {
|
|
1182
1207
|
let requestedInteractiveTerm = false;
|
|
1183
1208
|
while (true) {
|
|
@@ -1189,16 +1214,9 @@ function readTermFromStream(stream, solver) {
|
|
|
1189
1214
|
throw new PrologError('syntax_error(read_term)');
|
|
1190
1215
|
}
|
|
1191
1216
|
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');
|
|
1217
|
+
const term = parseReadTermText(candidate.text, solver);
|
|
1200
1218
|
stream.position = candidate.end;
|
|
1201
|
-
return scopeReadTerm(
|
|
1219
|
+
return scopeReadTerm(term);
|
|
1202
1220
|
} catch (_) {
|
|
1203
1221
|
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1204
1222
|
// 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,31 @@ 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
|
+
|
|
755
780
|
let error = null;
|
|
756
781
|
try {
|
|
757
782
|
runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
|
|
@@ -769,6 +794,15 @@ c4 ?- call((!;1)).
|
|
|
769
794
|
assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
|
|
770
795
|
assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
|
|
771
796
|
assertEqual(repl.stderr, '', 'REPL stderr');
|
|
797
|
+
|
|
798
|
+
const continuedGraphic = runCli([], {
|
|
799
|
+
input: 'read(T).\n!,*.\n.\nread(T).\na\n.\nhalt.\n',
|
|
800
|
+
});
|
|
801
|
+
assertEqual(continuedGraphic.status, 0, 'continued graphic operator REPL status');
|
|
802
|
+
assertIncludes(continuedGraphic.stdout, 'T = (!, *.).', 'continued graphic operator answer');
|
|
803
|
+
assertIncludes(continuedGraphic.stdout, 'T = a.', 'read after continued graphic operator');
|
|
804
|
+
assertNotIncludes(continuedGraphic.stdout, 'syntax_error', 'continued graphic operator syntax');
|
|
805
|
+
assertEqual(continuedGraphic.stderr, '', 'continued graphic operator REPL stderr');
|
|
772
806
|
},
|
|
773
807
|
},
|
|
774
808
|
{
|