eyeprolog 1.2.35 → 1.2.36
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 +6 -3
- package/src/syntax-scan.js +20 -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 +44 -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
|
}
|
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;
|
|
@@ -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,43 @@ 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
|
+
assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
|
|
769
|
+
assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
|
|
770
|
+
assertEqual(repl.stderr, '', 'REPL stderr');
|
|
771
|
+
},
|
|
772
|
+
},
|
|
736
773
|
{
|
|
737
774
|
name: 'question mark is a graphic character and writeq keeps graphic atoms unquoted',
|
|
738
775
|
run: () => {
|
|
@@ -2781,20 +2818,22 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2781
2818
|
const goal = parseGoalText('trial(Chars)');
|
|
2782
2819
|
let count = 0;
|
|
2783
2820
|
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2784
|
-
if (++count ===
|
|
2821
|
+
if (++count === 50000) break;
|
|
2785
2822
|
}
|
|
2786
|
-
if (count !==
|
|
2823
|
+
if (count !== 50000) throw new Error('unexpected answer count: ' + count);
|
|
2787
2824
|
process.stdout.write(String(count));
|
|
2788
2825
|
`;
|
|
2789
2826
|
const result = spawnSync(process.execPath, [
|
|
2790
|
-
|
|
2827
|
+
// A smaller heap preserves the original false-exhaustion signal
|
|
2828
|
+
// without requiring half a million transient conversion attempts.
|
|
2829
|
+
'--max-old-space-size=32',
|
|
2791
2830
|
'--input-type=module',
|
|
2792
2831
|
'--eval',
|
|
2793
2832
|
script,
|
|
2794
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout:
|
|
2833
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
2795
2834
|
if (result.error) throw result.error;
|
|
2796
2835
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2797
|
-
assertEqual(result.stdout, '
|
|
2836
|
+
assertEqual(result.stdout, '50000', 'distinct number syntax attempts');
|
|
2798
2837
|
},
|
|
2799
2838
|
},
|
|
2800
2839
|
{
|
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
|