eyeprolog 1.2.35 → 1.2.37
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 +3 -15
- package/src/parser.js +6 -5
- package/src/repl.js +7 -3
- package/src/syntax-scan.js +20 -0
- package/src/write.js +11 -0
- package/test/conformance/cases/iso/streams_and_term_io.pl +1 -1
- package/test/conformance/expected/iso/streams_and_term_io.pl +1 -1
- package/test/conformance/expected-errors/syntax/extra_double_period_rejected.txt +1 -1
- package/test/run-regression.mjs +45 -5
- package/the-art-of-eyeprolog.md +10 -3
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -9,7 +9,9 @@ import { sameNumberValue } from './number-value.js';
|
|
|
9
9
|
import { createParserOperatorState, parseClauses, parseGoalText, parseNumberTokenText } from './parser.js';
|
|
10
10
|
import { formatTermForWrite } from './write.js';
|
|
11
11
|
import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
|
|
14
|
+
} from './syntax-scan.js';
|
|
13
15
|
|
|
14
16
|
let isoFresh = 0;
|
|
15
17
|
|
|
@@ -1072,20 +1074,6 @@ function* nlBuiltin({ solver, goal, env }) {
|
|
|
1072
1074
|
yield env;
|
|
1073
1075
|
}
|
|
1074
1076
|
|
|
1075
|
-
function isTerminatingFullStop(source, index) {
|
|
1076
|
-
const previous = source[index - 1] ?? '';
|
|
1077
|
-
const next = source[index + 1] ?? '';
|
|
1078
|
-
if (previous === '.' || next === '.') return false;
|
|
1079
|
-
if (/\d/.test(previous) && /\d/.test(next)) return false;
|
|
1080
|
-
if (/[A-Za-z0-9_]/.test(previous) && /[A-Za-z0-9_]/.test(next)) return false;
|
|
1081
|
-
return true;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const termGraphicCharacters = new Set('#$&*+-./<=>?@^~\\:');
|
|
1085
|
-
function continuesGraphicToken(source, index) {
|
|
1086
|
-
return index > 0 && termGraphicCharacters.has(source[index - 1]);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
1077
|
function* termTextCandidates(stream) {
|
|
1090
1078
|
const source = String(stream.content);
|
|
1091
1079
|
let quote = null, lineComment = false, blockComment = false;
|
package/src/parser.js
CHANGED
|
@@ -1,6 +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
5
|
|
|
5
6
|
const TOK = {
|
|
6
7
|
EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
|
|
@@ -369,12 +370,11 @@ class Parser {
|
|
|
369
370
|
this.pos += 2;
|
|
370
371
|
return { type: TOK.ATOM, text: '?-', line };
|
|
371
372
|
}
|
|
372
|
-
if (ch === '.' && this.
|
|
373
|
-
!isWhitespaceCode(this.peek(1).charCodeAt(0)) &&
|
|
374
|
-
this.peek(1) !== '%' && !(this.peek(1) === '/' && this.peek(2) === '*')) {
|
|
373
|
+
if (ch === '.' && !isTerminatingFullStop(this.source, this.pos)) {
|
|
375
374
|
const start = this.pos;
|
|
376
375
|
this.take();
|
|
377
|
-
while (isGraphicAtomCode(this.peek().charCodeAt(0))
|
|
376
|
+
while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
|
|
377
|
+
!isTerminatingFullStop(this.source, this.pos)) this.take();
|
|
378
378
|
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
379
379
|
}
|
|
380
380
|
if (ch === '!') {
|
|
@@ -543,7 +543,8 @@ class Parser {
|
|
|
543
543
|
if (isGraphicAtomCode(ch.charCodeAt(0))) {
|
|
544
544
|
const start = this.pos;
|
|
545
545
|
this.take();
|
|
546
|
-
while (isGraphicAtomCode(this.peek().charCodeAt(0))
|
|
546
|
+
while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
|
|
547
|
+
!isTerminatingFullStop(this.source, this.pos)) this.take();
|
|
547
548
|
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
548
549
|
}
|
|
549
550
|
|
package/src/repl.js
CHANGED
|
@@ -4,7 +4,9 @@ import { readSync } from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { formalErrorTerm } from './iso.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
|
|
9
|
+
} from './syntax-scan.js';
|
|
8
10
|
|
|
9
11
|
const ANSWER_HELP = `
|
|
10
12
|
SPACE, "n" or ";": next solution, if any
|
|
@@ -389,7 +391,7 @@ function terminalFullStop(source) {
|
|
|
389
391
|
lineComment = true;
|
|
390
392
|
continue;
|
|
391
393
|
}
|
|
392
|
-
if (ch === '/' && next === '*') {
|
|
394
|
+
if (ch === '/' && next === '*' && !continuesGraphicToken(source, i)) {
|
|
393
395
|
blockComment = true;
|
|
394
396
|
i++;
|
|
395
397
|
continue;
|
|
@@ -400,7 +402,8 @@ function terminalFullStop(source) {
|
|
|
400
402
|
}
|
|
401
403
|
if ('([{'.includes(ch)) depth++;
|
|
402
404
|
else if (')]}'.includes(ch)) depth = Math.max(0, depth - 1);
|
|
403
|
-
else if (ch === '.' && depth === 0 &&
|
|
405
|
+
else if (ch === '.' && depth === 0 && isTerminatingFullStop(source, i) &&
|
|
406
|
+
onlyLayoutAndComments(source.slice(i + 1))) return i;
|
|
404
407
|
}
|
|
405
408
|
return -1;
|
|
406
409
|
}
|
|
@@ -614,6 +617,7 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
614
617
|
// current operator atoms in argument and list-element positions without
|
|
615
618
|
// quotes, just as writeq/1 already prints them.
|
|
616
619
|
operatorAtomsAsArgs: true,
|
|
620
|
+
dottedGraphicAtoms: true,
|
|
617
621
|
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
618
622
|
})}`);
|
|
619
623
|
}
|
package/src/syntax-scan.js
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
// readers. These locate token boundaries only; parser.js remains responsible
|
|
3
3
|
// for accepting or rejecting the token itself.
|
|
4
4
|
|
|
5
|
+
const graphicTokenCharacters = new Set('#$&*+-./<=>?@^~\\:');
|
|
6
|
+
|
|
7
|
+
export function continuesGraphicToken(source, index) {
|
|
8
|
+
return index > 0 && graphicTokenCharacters.has(source[index - 1]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isTerminatingFullStop(source, index) {
|
|
12
|
+
if (source[index] !== '.') return false;
|
|
13
|
+
const next = source[index + 1] ?? '';
|
|
14
|
+
// At a line boundary, after a single-line comment marker, or at end of
|
|
15
|
+
// input, the dot is the read-term end char. Before horizontal layout it is
|
|
16
|
+
// instead part of an already-started graphic token: `./*. .` is the atom
|
|
17
|
+
// `./*.` followed by its separate end char. A directly following /* also
|
|
18
|
+
// stays in the current graphic token; bracketed comments are recognized
|
|
19
|
+
// only when /* begins a token.
|
|
20
|
+
if (next === '' || next === '%' || next === '\n' || next === '\r') return true;
|
|
21
|
+
if (/^[\u0009\u000b\u000c\u0020]$/.test(next)) return !continuesGraphicToken(source, index);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export function quotedEscapeEnd(source, index) {
|
|
6
26
|
const escaped = source[index + 1] ?? '';
|
|
7
27
|
if (!escaped) return index;
|
package/src/write.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './term.js';
|
|
6
6
|
|
|
7
7
|
const graphicAtomCharacters = new Set('!#$&*+-/<=>?@^~\\'.split(''));
|
|
8
|
+
const dottedGraphicAtomCharacters = new Set([...graphicAtomCharacters, '.']);
|
|
8
9
|
const compactInfixOperators = new Set([':', '..']);
|
|
9
10
|
|
|
10
11
|
function quotedControlEscape(ch) {
|
|
@@ -48,6 +49,11 @@ function writeAtom(name) {
|
|
|
48
49
|
return atomNeedsQuotes(name) ? quoteAtom(name) : name;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function isDottedGraphicAtom(name) {
|
|
53
|
+
return name.includes('.') && [...name].some((ch) => ch !== '.') && !name.startsWith('/*') &&
|
|
54
|
+
[...name].every((ch) => dottedGraphicAtomCharacters.has(ch));
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
function legacyVariableToIso(name) {
|
|
52
58
|
if (name === '?') return '_';
|
|
53
59
|
const tail = name.slice(1);
|
|
@@ -172,6 +178,10 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
172
178
|
if (resolved.type === STRING) return writeString(resolved.name);
|
|
173
179
|
if (resolved.type === ATOM) {
|
|
174
180
|
if (!options.quoted) return resolved.name;
|
|
181
|
+
// Top-level bindings are already delimited by their answer punctuation.
|
|
182
|
+
// Keep valid dotted graphic tokens readable there without weakening the
|
|
183
|
+
// ISO writeq/1 policy tested by WG17 #308.
|
|
184
|
+
if (options.dottedGraphicAtoms && isDottedGraphicAtom(resolved.name)) return resolved.name;
|
|
175
185
|
// ISO 6.3.3.1 gives functional arguments and list elements a special
|
|
176
186
|
// `arg` production: an atom that is a current operator is valid there
|
|
177
187
|
// without quoting. Keep lexical exceptions such as `|` quoted.
|
|
@@ -269,6 +279,7 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
|
|
|
269
279
|
variableNames: printableReadVariableNames(term, env, explicitVariableNames),
|
|
270
280
|
compact: options.compact === true,
|
|
271
281
|
operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
|
|
282
|
+
dottedGraphicAtoms: options.dottedGraphicAtoms === true,
|
|
272
283
|
};
|
|
273
284
|
const maxPriority = Number.isInteger(options.maxPriority)
|
|
274
285
|
? Math.max(0, Math.min(1200, options.maxPriority))
|
|
@@ -5,7 +5,7 @@ text_roundtrip(Term, Peek, Code, Mode, Alias) :-
|
|
|
5
5
|
open('/tmp/eyeprolog-iso-text.txt', write, Output, [alias(iso_text_output), type(text)]),
|
|
6
6
|
writeq(iso_text_output, sample(42)),
|
|
7
7
|
put_char(iso_text_output, '.'),
|
|
8
|
-
put_char(iso_text_output, '
|
|
8
|
+
put_char(iso_text_output, ' '),
|
|
9
9
|
close(Output),
|
|
10
10
|
open('/tmp/eyeprolog-iso-text.txt', read, Input, [alias(iso_text_input), eof_action(eof_code)]),
|
|
11
11
|
stream_property(Input, mode(Mode)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1: expected ., got
|
|
1
|
+
parse line 1: expected ., got .
|
package/test/run-regression.mjs
CHANGED
|
@@ -733,6 +733,44 @@ c4 ?- call((!;1)).
|
|
|
733
733
|
assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
|
|
734
734
|
},
|
|
735
735
|
},
|
|
736
|
+
{
|
|
737
|
+
name: 'readers distinguish graphic tokens, comments, and full stops (issue #41)',
|
|
738
|
+
run: () => {
|
|
739
|
+
const parsed = parseProgramText('./*.');
|
|
740
|
+
assertEqual(parsed.length, 1, 'graphic atom clause count');
|
|
741
|
+
assertEqual(parsed[0].head.name, './*', 'comment opener stays inside graphic atom');
|
|
742
|
+
|
|
743
|
+
const read = runEyeProlog('', {
|
|
744
|
+
goal: 'read(T)',
|
|
745
|
+
ioOptions: { input: './*.' },
|
|
746
|
+
});
|
|
747
|
+
assertEqual(read.stdout, "read('./*').\n", 'graphic atom writeq readback');
|
|
748
|
+
|
|
749
|
+
const consecutive = runEyeProlog('answer(A, B) :- read(A), read(B).\n', {
|
|
750
|
+
goal: 'answer(A, B)',
|
|
751
|
+
ioOptions: { input: './*. .\nok.\n' },
|
|
752
|
+
});
|
|
753
|
+
assertEqual(consecutive.stdout, "answer('./*.', ok).\n", 'following read starts after the complete term');
|
|
754
|
+
|
|
755
|
+
let error = null;
|
|
756
|
+
try {
|
|
757
|
+
runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
|
|
758
|
+
} catch (caught) {
|
|
759
|
+
error = caught;
|
|
760
|
+
}
|
|
761
|
+
assertEqual(error?.message, 'error(syntax_error(read_term))', 'solo-token sequence rejection');
|
|
762
|
+
|
|
763
|
+
const repl = runCli([], {
|
|
764
|
+
input: 'read(T).\n./*. .\nread(T).\nok.\nread(T).\n!.!.\nhalt.\n',
|
|
765
|
+
});
|
|
766
|
+
assertEqual(repl.status, 0, 'REPL exit status');
|
|
767
|
+
assertIncludes(repl.stdout, 'T = ./*..', 'REPL dotted graphic atom answer');
|
|
768
|
+
assertNotIncludes(repl.stdout, "T = './*.'", 'REPL dotted graphic atom has no spurious quotes');
|
|
769
|
+
assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
|
|
770
|
+
assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
|
|
771
|
+
assertEqual(repl.stderr, '', 'REPL stderr');
|
|
772
|
+
},
|
|
773
|
+
},
|
|
736
774
|
{
|
|
737
775
|
name: 'question mark is a graphic character and writeq keeps graphic atoms unquoted',
|
|
738
776
|
run: () => {
|
|
@@ -2781,20 +2819,22 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2781
2819
|
const goal = parseGoalText('trial(Chars)');
|
|
2782
2820
|
let count = 0;
|
|
2783
2821
|
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2784
|
-
if (++count ===
|
|
2822
|
+
if (++count === 50000) break;
|
|
2785
2823
|
}
|
|
2786
|
-
if (count !==
|
|
2824
|
+
if (count !== 50000) throw new Error('unexpected answer count: ' + count);
|
|
2787
2825
|
process.stdout.write(String(count));
|
|
2788
2826
|
`;
|
|
2789
2827
|
const result = spawnSync(process.execPath, [
|
|
2790
|
-
|
|
2828
|
+
// A smaller heap preserves the original false-exhaustion signal
|
|
2829
|
+
// without requiring half a million transient conversion attempts.
|
|
2830
|
+
'--max-old-space-size=32',
|
|
2791
2831
|
'--input-type=module',
|
|
2792
2832
|
'--eval',
|
|
2793
2833
|
script,
|
|
2794
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout:
|
|
2834
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
2795
2835
|
if (result.error) throw result.error;
|
|
2796
2836
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2797
|
-
assertEqual(result.stdout, '
|
|
2837
|
+
assertEqual(result.stdout, '50000', 'distinct number syntax attempts');
|
|
2798
2838
|
},
|
|
2799
2839
|
},
|
|
2800
2840
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5369,9 +5369,16 @@ comment continues to the end of its line. Doubling the active delimiter is
|
|
|
5369
5369
|
also accepted inside either quoted form, so `""` inside double-quoted notation
|
|
5370
5370
|
denotes one literal double quote character.
|
|
5371
5371
|
|
|
5372
|
-
Graphic
|
|
5373
|
-
qualification operator in `Module:Goal`;
|
|
5374
|
-
contains a colon. Unquoted angle-bracket IRIs
|
|
5372
|
+
Graphic tokens use the characters `#$&*+-./<=>?@^~\`; `!` and `;` are solo
|
|
5373
|
+
atoms. A colon is the Part 2 module qualification operator in `Module:Goal`;
|
|
5374
|
+
quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
|
|
5375
|
+
are not syntax.
|
|
5376
|
+
|
|
5377
|
+
A `/*` sequence opens a block comment only when it begins a token; inside a
|
|
5378
|
+
maximal graphic token the slash and star remain atom characters. A period ends
|
|
5379
|
+
a term only when it is recognized as the terminating full stop. Consequently,
|
|
5380
|
+
`./*.` at the end of a line reads the atom `./*`, whereas `./*. .` reads the
|
|
5381
|
+
atom `./*.` and consumes the second period as the terminator.
|
|
5375
5382
|
|
|
5376
5383
|
In the grammar below, `{ x }` means zero or more repetitions of `x`, `[ x ]`
|
|
5377
5384
|
means that `x` is optional, and parentheses group alternatives. These marks
|