eyeprolog 1.2.34 → 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 +18 -26
- 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 +70 -5
- package/the-art-of-eyeprolog.md +23 -7
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;
|
|
@@ -2029,35 +2017,39 @@ function* negationBuiltin({ solver, goal, env }) {
|
|
|
2029
2017
|
for (const _ of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) return;
|
|
2030
2018
|
yield env;
|
|
2031
2019
|
}
|
|
2020
|
+
function* solveControlBranch(solver, goal, env) {
|
|
2021
|
+
for (const answer of solver.solve([callable(goal, env)], env, 0)) {
|
|
2022
|
+
// A branch answer is internal to its enclosing control construct. The
|
|
2023
|
+
// surrounding solve will count the completed control goal after the
|
|
2024
|
+
// builtin yields it. Leaving both counts in place makes a bounded search
|
|
2025
|
+
// such as once/1 or negation stop before it can observe the branch answer.
|
|
2026
|
+
if (solver.solutionsSeen > 0) solver.solutionsSeen--;
|
|
2027
|
+
yield answer;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2032
2030
|
function* disjunctionBuiltin({ solver, goal, env }) {
|
|
2033
2031
|
const left = deref(goal.args[0], env);
|
|
2034
2032
|
if (left.type === COMPOUND && left.name === '->' && left.arity === 2) {
|
|
2035
2033
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(left.args[0], env)], env.clone(), 0)) {
|
|
2036
|
-
yield* solver
|
|
2034
|
+
yield* solveControlBranch(solver, left.args[1], conditionEnv);
|
|
2037
2035
|
return;
|
|
2038
2036
|
}
|
|
2039
|
-
yield* solver
|
|
2037
|
+
yield* solveControlBranch(solver, goal.args[1], env.clone());
|
|
2040
2038
|
return;
|
|
2041
2039
|
}
|
|
2042
2040
|
const marker = solver.active[solver.active.length - 1] ?? null;
|
|
2043
2041
|
const markerCutEpoch = marker?.cutEpoch ?? 0;
|
|
2044
2042
|
const solverCutEpoch = solver.cutEpoch;
|
|
2045
|
-
yield* solver
|
|
2043
|
+
yield* solveControlBranch(solver, goal.args[0], env.clone());
|
|
2046
2044
|
const cutThisScope = marker == null
|
|
2047
2045
|
? solver.cutEpoch !== solverCutEpoch
|
|
2048
2046
|
: (marker.cutEpoch ?? 0) !== markerCutEpoch;
|
|
2049
2047
|
if (cutThisScope) return;
|
|
2050
|
-
yield* solver
|
|
2048
|
+
yield* solveControlBranch(solver, goal.args[1], env.clone());
|
|
2051
2049
|
}
|
|
2052
2050
|
function* ifThenBuiltin({ solver, goal, env }) {
|
|
2053
2051
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|
|
2054
|
-
|
|
2055
|
-
// The consequent is an internal part of the current solution, not a
|
|
2056
|
-
// completed top-level solution. Keep a surrounding bounded search (for
|
|
2057
|
-
// example nested ISO once-as-if-then) from consuming its limit early.
|
|
2058
|
-
if (solver.solutionsSeen > 0) solver.solutionsSeen--;
|
|
2059
|
-
yield consequentEnv;
|
|
2060
|
-
}
|
|
2052
|
+
yield* solveControlBranch(solver, goal.args[1], conditionEnv);
|
|
2061
2053
|
return;
|
|
2062
2054
|
}
|
|
2063
2055
|
}
|
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
|
@@ -477,6 +477,32 @@ c4 ?- call((!;1)).
|
|
|
477
477
|
assertEqual(result.stdout, 'quads: 22 run, 22 passed, 0 failed.\n', 'quad report');
|
|
478
478
|
},
|
|
479
479
|
},
|
|
480
|
+
{
|
|
481
|
+
name: 'negation observes disjunction through direct and call/1 execution',
|
|
482
|
+
run: () => {
|
|
483
|
+
const reported = publicApi.runQuads(String.raw`?- \+ (true ; true).
|
|
484
|
+
false.
|
|
485
|
+
|
|
486
|
+
?- call(\+ (true ; true)).
|
|
487
|
+
false.
|
|
488
|
+
`);
|
|
489
|
+
assertEqual(reported.total, 2, 'reported query count');
|
|
490
|
+
assertEqual(reported.passed, 2, 'reported queries pass');
|
|
491
|
+
|
|
492
|
+
const program = Program.parse('');
|
|
493
|
+
const answerCount = (text) => {
|
|
494
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
495
|
+
return [...solver.solve([parseGoalText(text)], new Env(), 0)].length;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
assertEqual(answerCount(String.raw`\+ (true ; true)`), 0, 'direct successful disjunction is negated');
|
|
499
|
+
assertEqual(answerCount(String.raw`call(\+ (true ; true))`), 0, 'called negation fails');
|
|
500
|
+
assertEqual(answerCount(String.raw`\+ (true ; fail)`), 0, 'successful left branch is observed');
|
|
501
|
+
assertEqual(answerCount(String.raw`\+ (fail ; true)`), 0, 'successful right branch is observed');
|
|
502
|
+
assertEqual(answerCount(String.raw`\+ (fail ; fail)`), 1, 'failed disjunction is negated');
|
|
503
|
+
assertEqual(answerCount('once((true ; true))'), 1, 'once keeps the first disjunction answer');
|
|
504
|
+
},
|
|
505
|
+
},
|
|
480
506
|
{
|
|
481
507
|
name: 'runQuads passes the complete vendored ISO phrase quad corpus',
|
|
482
508
|
run: () => {
|
|
@@ -707,6 +733,43 @@ c4 ?- call((!;1)).
|
|
|
707
733
|
assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
|
|
708
734
|
},
|
|
709
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
|
+
},
|
|
710
773
|
{
|
|
711
774
|
name: 'question mark is a graphic character and writeq keeps graphic atoms unquoted',
|
|
712
775
|
run: () => {
|
|
@@ -2755,20 +2818,22 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2755
2818
|
const goal = parseGoalText('trial(Chars)');
|
|
2756
2819
|
let count = 0;
|
|
2757
2820
|
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2758
|
-
if (++count ===
|
|
2821
|
+
if (++count === 50000) break;
|
|
2759
2822
|
}
|
|
2760
|
-
if (count !==
|
|
2823
|
+
if (count !== 50000) throw new Error('unexpected answer count: ' + count);
|
|
2761
2824
|
process.stdout.write(String(count));
|
|
2762
2825
|
`;
|
|
2763
2826
|
const result = spawnSync(process.execPath, [
|
|
2764
|
-
|
|
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',
|
|
2765
2830
|
'--input-type=module',
|
|
2766
2831
|
'--eval',
|
|
2767
2832
|
script,
|
|
2768
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout:
|
|
2833
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
2769
2834
|
if (result.error) throw result.error;
|
|
2770
2835
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2771
|
-
assertEqual(result.stdout, '
|
|
2836
|
+
assertEqual(result.stdout, '50000', 'distinct number syntax attempts');
|
|
2772
2837
|
},
|
|
2773
2838
|
},
|
|
2774
2839
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1844,7 +1844,10 @@ normalized at the solver boundary instead of leaking a JavaScript `RangeError`.
|
|
|
1844
1844
|
ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
|
|
1845
1845
|
`memory` for a finite host allocation/capacity ceiling and reserves the
|
|
1846
1846
|
`finite_memory` spelling for the distinct convention where no finite amount of
|
|
1847
|
-
memory could complete the computation.
|
|
1847
|
+
memory could complete the computation. After a recoverable memory error, the
|
|
1848
|
+
solver keeps a bounded recovery window while the failed search unwinds so the
|
|
1849
|
+
host can collect released query terms. The same solver can then run later
|
|
1850
|
+
queries; this recovery does not resume the query that exhausted its limit.
|
|
1848
1851
|
|
|
1849
1852
|
The iterative solver keeps active-call frames only where they are semantically
|
|
1850
1853
|
needed for cut scope or recursive variant guards. Bundled-library helpers whose
|
|
@@ -1853,9 +1856,15 @@ guard therefore do not copy a growing active-call sequence at every step.
|
|
|
1853
1856
|
Under the normal EyeProlog registry, the bundled Prologue `length/2` also has a
|
|
1854
1857
|
scoped iterative execution path: named lists are counted or constructed without
|
|
1855
1858
|
recursive interpreter frames, and an anonymous list is not materialized because
|
|
1856
|
-
its binding cannot be observed.
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
+
its binding cannot be observed. A newly constructed fixed-length suffix starts
|
|
1860
|
+
as a lazy compact skeleton and expands one ordinary `./2` cell at a time when
|
|
1861
|
+
unification, another list predicate, or answer readback inspects it. This is a
|
|
1862
|
+
storage optimization, not a distinct Prolog term or list semantics. Embedders
|
|
1863
|
+
that inspect the JavaScript term model can recognize this representation with
|
|
1864
|
+
`CompactListTerm`, `isCompactList`, and `compactListLength`, or construct one
|
|
1865
|
+
with `compactVariableList`. The ordinary clauses remain the authoritative module
|
|
1866
|
+
definition and are used unchanged by the ISO-only registry and whenever delays
|
|
1867
|
+
or finite-domain constraints require their normal wake-up points.
|
|
1859
1868
|
|
|
1860
1869
|
### Implementation boundary
|
|
1861
1870
|
|
|
@@ -5360,9 +5369,16 @@ comment continues to the end of its line. Doubling the active delimiter is
|
|
|
5360
5369
|
also accepted inside either quoted form, so `""` inside double-quoted notation
|
|
5361
5370
|
denotes one literal double quote character.
|
|
5362
5371
|
|
|
5363
|
-
Graphic
|
|
5364
|
-
qualification operator in `Module:Goal`;
|
|
5365
|
-
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.
|
|
5366
5382
|
|
|
5367
5383
|
In the grammar below, `{ x }` means zero or more repetitions of `x`, `[ x ]`
|
|
5368
5384
|
means that `x` is optional, and parentheses group alternatives. These marks
|