eyeprolog 1.1.1 → 1.1.3
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/README.md +12 -0
- package/conformance-report.md +2 -2
- package/package.json +1 -1
- package/src/cli.js +15 -3
- package/src/dcg.js +4 -0
- package/src/repl.js +412 -0
- package/test/conformance/ISO-MATRIX.md +1 -1
- package/test/conformance/README.md +3 -3
- package/test/conformance/THIRD_PARTY.md +7 -0
- package/test/conformance/cases/iso/dcg_ambiguous_alternatives.pl +8 -0
- package/test/conformance/cases/iso/dcg_declaration_nonterminal_indicators.pl +7 -0
- package/test/conformance/cases/iso/dcg_double_quotes_atom.pl +7 -0
- package/test/conformance/cases/iso/dcg_double_quotes_codes.pl +6 -0
- package/test/conformance/cases/iso/dcg_dynamic_body_control.pl +15 -0
- package/test/conformance/cases/iso/dcg_dynamic_nonterminal_indicator.pl +7 -0
- package/test/conformance/cases/iso/dcg_explicit_qualified_head.pl +6 -0
- package/test/conformance/cases/iso/dcg_nested_phrase_call.pl +6 -0
- package/test/conformance/cases/iso/dcg_operator_table.pl +5 -0
- package/test/conformance/cases/iso/dcg_partial_sequences.pl +12 -0
- package/test/conformance/cases/iso/dcg_phrase_steadfastness.pl +14 -0
- package/test/conformance/cases/iso/dcg_qualified_chain.pl +10 -0
- package/test/conformance/cases/iso/dcg_recursive_sequences.pl +8 -0
- package/test/conformance/cases/iso/dcg_variable_hygiene.pl +9 -0
- package/test/conformance/cases/iso/logtalk_dcg_conjunction_disjunction.pl +14 -0
- package/test/conformance/cases/iso/logtalk_dcg_cut.pl +12 -0
- package/test/conformance/cases/iso/logtalk_dcg_cut_positions.pl +13 -0
- package/test/conformance/cases/iso/logtalk_dcg_embedded_goals.pl +12 -0
- package/test/conformance/cases/iso/logtalk_dcg_embedded_sequences.pl +10 -0
- package/test/conformance/cases/iso/logtalk_dcg_graphic_nonterminal.pl +7 -0
- package/test/conformance/cases/iso/logtalk_dcg_if_then_else.pl +15 -0
- package/test/conformance/cases/iso/logtalk_dcg_negation_positions.pl +11 -0
- package/test/conformance/cases/iso/logtalk_dcg_nonterminals.pl +11 -0
- package/test/conformance/cases/iso/logtalk_dcg_phrase_identity.pl +12 -0
- package/test/conformance/cases/iso/logtalk_dcg_semicontext_variants.pl +12 -0
- package/test/conformance/cases/iso/logtalk_dcg_semicontexts.pl +12 -0
- package/test/conformance/cases/iso/logtalk_dcg_terminal_variables.pl +9 -0
- package/test/conformance/cases/iso/logtalk_dcg_terminals.pl +15 -0
- package/test/conformance/cases/iso/logtalk_dcg_variable_bodies.pl +10 -0
- package/test/conformance/errors/iso/dcg_call_noncallable.pl +3 -0
- package/test/conformance/errors/iso/dcg_call_variable.pl +3 -0
- package/test/conformance/errors/iso/dcg_nonatom_qualified_module.pl +3 -0
- package/test/conformance/errors/iso/dcg_partial_semicontext.pl +3 -0
- package/test/conformance/errors/iso/dcg_phrase_nonterminal_variable.pl +3 -0
- package/test/conformance/errors/iso/dcg_qualified_unknown_nonterminal.pl +4 -0
- package/test/conformance/errors/iso/dcg_variable_qualified_module.pl +3 -0
- package/test/conformance/errors/iso/dcg_variable_rule_body.pl +3 -0
- package/test/conformance/errors/iso/dcg_variable_rule_head.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_bad_input_precedence.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_bad_output_precedence.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_curly_head.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_cut_head.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_embedded_noncallable.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_improper_semicontext.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_improper_terminal_rule.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_invalid_semicontext.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_multiple_semicontexts.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_nested_noncallable.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_noncallable_after_failure.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_noncallable_after_success.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_noncallable_precedence.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_numeric_head.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_numeric_nonterminal.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_partial_terminal_rule.pl +4 -0
- package/test/conformance/errors/iso/logtalk_dcg_phrase_curly_variable.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_phrase_improper_input.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_phrase_improper_output.pl +2 -0
- package/test/conformance/errors/iso/logtalk_dcg_reversed_semicontext.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_terminal_head.pl +3 -0
- package/test/conformance/errors/iso/logtalk_dcg_unknown_nonterminal.pl +2 -0
- package/test/conformance/expected/iso/dcg_ambiguous_alternatives.pl +1 -0
- package/test/conformance/expected/iso/dcg_declaration_nonterminal_indicators.pl +1 -0
- package/test/conformance/expected/iso/dcg_double_quotes_atom.pl +1 -0
- package/test/conformance/expected/iso/dcg_double_quotes_codes.pl +1 -0
- package/test/conformance/expected/iso/dcg_dynamic_body_control.pl +5 -0
- package/test/conformance/expected/iso/dcg_dynamic_nonterminal_indicator.pl +1 -0
- package/test/conformance/expected/iso/dcg_explicit_qualified_head.pl +1 -0
- package/test/conformance/expected/iso/dcg_nested_phrase_call.pl +1 -0
- package/test/conformance/expected/iso/dcg_operator_table.pl +2 -0
- package/test/conformance/expected/iso/dcg_partial_sequences.pl +3 -0
- package/test/conformance/expected/iso/dcg_phrase_steadfastness.pl +4 -0
- package/test/conformance/expected/iso/dcg_qualified_chain.pl +2 -0
- package/test/conformance/expected/iso/dcg_recursive_sequences.pl +3 -0
- package/test/conformance/expected/iso/dcg_variable_hygiene.pl +2 -0
- package/test/conformance/expected/iso/logtalk_dcg_conjunction_disjunction.pl +8 -0
- package/test/conformance/expected/iso/logtalk_dcg_cut.pl +4 -0
- package/test/conformance/expected/iso/logtalk_dcg_cut_positions.pl +4 -0
- package/test/conformance/expected/iso/logtalk_dcg_embedded_goals.pl +4 -0
- package/test/conformance/expected/iso/logtalk_dcg_embedded_sequences.pl +3 -0
- package/test/conformance/expected/iso/logtalk_dcg_graphic_nonterminal.pl +1 -0
- package/test/conformance/expected/iso/logtalk_dcg_if_then_else.pl +7 -0
- package/test/conformance/expected/iso/logtalk_dcg_negation_positions.pl +4 -0
- package/test/conformance/expected/iso/logtalk_dcg_nonterminals.pl +3 -0
- package/test/conformance/expected/iso/logtalk_dcg_phrase_identity.pl +6 -0
- package/test/conformance/expected/iso/logtalk_dcg_semicontext_variants.pl +4 -0
- package/test/conformance/expected/iso/logtalk_dcg_semicontexts.pl +4 -0
- package/test/conformance/expected/iso/logtalk_dcg_terminal_variables.pl +3 -0
- package/test/conformance/expected/iso/logtalk_dcg_terminals.pl +6 -0
- package/test/conformance/expected/iso/logtalk_dcg_variable_bodies.pl +4 -0
- package/test/conformance/expected-errors/iso/dcg_call_noncallable.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_call_variable.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_nonatom_qualified_module.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_partial_semicontext.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_phrase_nonterminal_variable.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_qualified_unknown_nonterminal.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_variable_qualified_module.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_variable_rule_body.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_variable_rule_head.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_bad_input_precedence.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_bad_output_precedence.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_curly_head.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_cut_head.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_embedded_noncallable.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_improper_semicontext.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_improper_terminal_rule.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_invalid_semicontext.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_multiple_semicontexts.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_nested_noncallable.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_failure.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_success.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_precedence.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_numeric_head.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_numeric_nonterminal.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_partial_terminal_rule.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_curly_variable.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_input.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_output.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_reversed_semicontext.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_terminal_head.txt +1 -0
- package/test/conformance/expected-errors/iso/logtalk_dcg_unknown_nonterminal.txt +1 -0
- package/test/run-regression.mjs +66 -3
- package/the-art-of-eyeprolog.md +28 -3
package/README.md
CHANGED
|
@@ -25,6 +25,18 @@ EyeProlog requires Node.js 18 or newer.
|
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
27
|
npm install --global eyeprolog
|
|
28
|
+
eyeprolog
|
|
29
|
+
?- use_module(library(lists)).
|
|
30
|
+
true.
|
|
31
|
+
?- member(X, [prolog, logic]).
|
|
32
|
+
X = prolog
|
|
33
|
+
; X = logic.
|
|
34
|
+
?- halt.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For a non-interactive run:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
28
40
|
printf 'human(socrates).\nmortal(X) :- human(X).\n' |
|
|
29
41
|
eyeprolog --proof --goal 'mortal(socrates)' -
|
|
30
42
|
```
|
package/conformance-report.md
CHANGED
|
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
11
11
|
| builtins | 11 | 0 | 0 | 0 | 11 |
|
|
12
12
|
| context | 11 | 0 | 0 | 0 | 11 |
|
|
13
13
|
| control | 15 | 0 | 0 | 0 | 15 |
|
|
14
|
-
| iso |
|
|
14
|
+
| iso | 164 | 209 | 0 | 0 | 373 |
|
|
15
15
|
| lists | 52 | 3 | 0 | 0 | 55 |
|
|
16
16
|
| modules | 2 | 0 | 0 | 0 | 2 |
|
|
17
17
|
| negation | 8 | 0 | 19 | 0 | 27 |
|
|
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
23
23
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
24
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
25
|
| variables | 16 | 9 | 0 | 0 | 25 |
|
|
26
|
-
| **Total** | **
|
|
26
|
+
| **Total** | **479** | **260** | **19** | **21** | **779** |
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -10,7 +10,14 @@ let explanationModule = null;
|
|
|
10
10
|
|
|
11
11
|
export async function main(argv) {
|
|
12
12
|
if (argv.length === 0) {
|
|
13
|
-
await
|
|
13
|
+
const engine = await loadEngine();
|
|
14
|
+
const { runRepl } = await import('./repl.js');
|
|
15
|
+
const exitCode = await runRepl(engine, {
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
errorOutput: process.stderr,
|
|
19
|
+
});
|
|
20
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
14
21
|
return;
|
|
15
22
|
}
|
|
16
23
|
|
|
@@ -109,15 +116,16 @@ export async function main(argv) {
|
|
|
109
116
|
|
|
110
117
|
async function loadEngine() {
|
|
111
118
|
if (engineModule == null) {
|
|
112
|
-
const [term, parser, program, solver, iso, library] = await Promise.all([
|
|
119
|
+
const [term, parser, program, solver, iso, library, write] = await Promise.all([
|
|
113
120
|
import('./term.js'),
|
|
114
121
|
import('./parser.js'),
|
|
115
122
|
import('./program.js'),
|
|
116
123
|
import('./solver.js'),
|
|
117
124
|
import('./iso.js'),
|
|
118
125
|
import('./standard-library.js'),
|
|
126
|
+
import('./write.js'),
|
|
119
127
|
]);
|
|
120
|
-
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library };
|
|
128
|
+
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write };
|
|
121
129
|
}
|
|
122
130
|
return engineModule;
|
|
123
131
|
}
|
|
@@ -186,8 +194,12 @@ async function usage(stream) {
|
|
|
186
194
|
stream.write(`eyeprolog ${await packageVersion()}
|
|
187
195
|
|
|
188
196
|
Usage:
|
|
197
|
+
eyeprolog
|
|
189
198
|
eyeprolog [options] [file-or-url.pl|- ...]
|
|
190
199
|
|
|
200
|
+
Interactive:
|
|
201
|
+
With no arguments, start a Prolog REPL. Use eyeprolog -h for help.
|
|
202
|
+
|
|
191
203
|
Input:
|
|
192
204
|
file-or-url.pl Read an EyeProlog program from a local file or http(s) URL.
|
|
193
205
|
- Read an EyeProlog program from standard input.
|
package/src/dcg.js
CHANGED
|
@@ -96,6 +96,10 @@ export function expandDcgBody(body, input, output, options = {}) {
|
|
|
96
96
|
return goal;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Empty braces are the empty embedded-goal sequence. Like {true}, they
|
|
100
|
+
// consume no terminals and leave the grammar state unchanged.
|
|
101
|
+
if (body.type === ATOM && body.name === '{}') return equality(input, output);
|
|
102
|
+
|
|
99
103
|
if (isTerminalSequence(body)) return terminalsGoal(body, input, output, env);
|
|
100
104
|
|
|
101
105
|
if (body.type === COMPOUND && body.name === ',' && body.arity === 2) {
|
package/src/repl.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// Interactive top level for the eyeprolog command.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
|
|
6
|
+
const ANSWER_HELP = `
|
|
7
|
+
SPACE, "n" or ";": next solution, if any
|
|
8
|
+
RETURN or ".": stop enumeration
|
|
9
|
+
"a": enumerate all solutions
|
|
10
|
+
"f": enumerate the next 5 solutions
|
|
11
|
+
"h": display this help message
|
|
12
|
+
"w": write terms without depth limit
|
|
13
|
+
"p": print terms with depth limit
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function runRepl(engine, options = {}) {
|
|
17
|
+
const input = options.input ?? process.stdin;
|
|
18
|
+
const output = options.output ?? process.stdout;
|
|
19
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
20
|
+
const reader = new LineReader(input, output);
|
|
21
|
+
const sources = [];
|
|
22
|
+
let state = makeState(engine, sources, output);
|
|
23
|
+
let exitCode = 0;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
state.solver.runInitializations();
|
|
27
|
+
while (true) {
|
|
28
|
+
const text = await readQuery(reader);
|
|
29
|
+
if (text == null) break;
|
|
30
|
+
if (!text.trim()) continue;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const goal = parseGoal(engine, state, text);
|
|
34
|
+
if (isUseModuleGoal(goal)) {
|
|
35
|
+
sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
|
|
36
|
+
state = makeState(engine, sources, output);
|
|
37
|
+
state.solver.runInitializations();
|
|
38
|
+
output.write(' true.\n');
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const consultFiles = consultDesignations(engine, goal);
|
|
42
|
+
if (consultFiles != null) {
|
|
43
|
+
for (const filename of consultFiles) sources.push(await readSource(filename));
|
|
44
|
+
state = makeState(engine, sources, output);
|
|
45
|
+
state.solver.runInitializations();
|
|
46
|
+
output.write(' true.\n');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await solveQuery(engine, state, goal, reader, output);
|
|
51
|
+
if (result?.halted) {
|
|
52
|
+
exitCode = result.code;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error?.name === 'HaltSignal') {
|
|
57
|
+
exitCode = error.code;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
output.write(` ${formatError(error)}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errorOutput.write(`eyeprolog: ${error?.message ?? String(error)}\n`);
|
|
65
|
+
exitCode = 1;
|
|
66
|
+
} finally {
|
|
67
|
+
reader.close();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return exitCode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class LineReader {
|
|
74
|
+
constructor(input, output) {
|
|
75
|
+
this.input = input;
|
|
76
|
+
this.output = output;
|
|
77
|
+
this.terminal = Boolean(input.isTTY && output.isTTY && typeof input.setRawMode === 'function');
|
|
78
|
+
this.history = [];
|
|
79
|
+
this.currentPrompt = '?- ';
|
|
80
|
+
this.open();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
open() {
|
|
84
|
+
this.readline = createInterface({
|
|
85
|
+
input: this.input,
|
|
86
|
+
output: this.output,
|
|
87
|
+
terminal: Boolean(this.input.isTTY && this.output.isTTY),
|
|
88
|
+
prompt: this.currentPrompt,
|
|
89
|
+
});
|
|
90
|
+
if (this.terminal && this.history.length > 0) {
|
|
91
|
+
this.readline.history.push(...this.history);
|
|
92
|
+
}
|
|
93
|
+
this.lines = this.readline[Symbol.asyncIterator]();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async read(prompt) {
|
|
97
|
+
this.currentPrompt = prompt;
|
|
98
|
+
this.readline.setPrompt(prompt);
|
|
99
|
+
this.output.write(prompt);
|
|
100
|
+
const result = await this.lines.next();
|
|
101
|
+
return result.done ? null : result.value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async readControl(prompt) {
|
|
105
|
+
if (!this.terminal) return this.read(prompt);
|
|
106
|
+
this.output.write(prompt);
|
|
107
|
+
this.history = [...this.readline.history];
|
|
108
|
+
this.currentPrompt = '?- ';
|
|
109
|
+
this.readline.close();
|
|
110
|
+
this.readline = null;
|
|
111
|
+
this.lines = null;
|
|
112
|
+
this.input.setRawMode(true);
|
|
113
|
+
this.input.resume();
|
|
114
|
+
|
|
115
|
+
const control = await new Promise((resolve, reject) => {
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
this.input.off('data', onData);
|
|
118
|
+
this.input.off('error', onError);
|
|
119
|
+
};
|
|
120
|
+
const onData = (data) => {
|
|
121
|
+
cleanup();
|
|
122
|
+
const text = String(data);
|
|
123
|
+
resolve(text === '\x04' ? null : text[0] ?? null);
|
|
124
|
+
};
|
|
125
|
+
const onError = (error) => {
|
|
126
|
+
cleanup();
|
|
127
|
+
reject(error);
|
|
128
|
+
};
|
|
129
|
+
this.input.once('data', onData);
|
|
130
|
+
this.input.once('error', onError);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.input.setRawMode(false);
|
|
134
|
+
this.open();
|
|
135
|
+
return control;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
close() {
|
|
139
|
+
if (this.input.isRaw) this.input.setRawMode(false);
|
|
140
|
+
this.readline?.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function makeState(engine, sources, output) {
|
|
145
|
+
const program = engine.Program.parseSources(sources);
|
|
146
|
+
const solver = new engine.Solver(program, {
|
|
147
|
+
registry: engine.getEyePrologRegistry(),
|
|
148
|
+
ioOptions: { write: (text) => output.write(String(text)) },
|
|
149
|
+
});
|
|
150
|
+
return { program: solver.program, solver };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readQuery(reader) {
|
|
154
|
+
let source = '';
|
|
155
|
+
let prompt = '?- ';
|
|
156
|
+
while (true) {
|
|
157
|
+
const line = await reader.read(prompt);
|
|
158
|
+
if (line == null) return source.trim() ? source : null;
|
|
159
|
+
source += `${line}\n`;
|
|
160
|
+
const end = terminalFullStop(source);
|
|
161
|
+
if (end >= 0) return source.slice(0, end);
|
|
162
|
+
prompt = '| ';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function terminalFullStop(source) {
|
|
167
|
+
let quote = null;
|
|
168
|
+
let lineComment = false;
|
|
169
|
+
let blockComment = false;
|
|
170
|
+
let escaped = false;
|
|
171
|
+
let depth = 0;
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < source.length; i++) {
|
|
174
|
+
const ch = source[i];
|
|
175
|
+
const next = source[i + 1];
|
|
176
|
+
if (lineComment) {
|
|
177
|
+
if (ch === '\n') lineComment = false;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (blockComment) {
|
|
181
|
+
if (ch === '*' && next === '/') {
|
|
182
|
+
blockComment = false;
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (quote != null) {
|
|
188
|
+
if (escaped) {
|
|
189
|
+
escaped = false;
|
|
190
|
+
} else if (ch === '\\') {
|
|
191
|
+
escaped = true;
|
|
192
|
+
} else if (ch === quote) {
|
|
193
|
+
if (next === quote) i++;
|
|
194
|
+
else quote = null;
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === '%') {
|
|
199
|
+
lineComment = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch === '/' && next === '*') {
|
|
203
|
+
blockComment = true;
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
208
|
+
quote = ch;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if ('([{'.includes(ch)) depth++;
|
|
212
|
+
else if (')]}'.includes(ch)) depth = Math.max(0, depth - 1);
|
|
213
|
+
else if (ch === '.' && depth === 0 && onlyLayoutAndComments(source.slice(i + 1))) return i;
|
|
214
|
+
}
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onlyLayoutAndComments(source) {
|
|
219
|
+
return source.replace(/\s|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g, '').length === 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseGoal(engine, state, text) {
|
|
223
|
+
const goal = engine.parseGoalText(text, {
|
|
224
|
+
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
225
|
+
operatorDefinitions: [...state.program.operators.values()],
|
|
226
|
+
});
|
|
227
|
+
if (goal.type === 'var') throw new engine.PrologError('instantiation_error');
|
|
228
|
+
if (goal.type !== 'atom' && goal.type !== 'compound') {
|
|
229
|
+
throw new engine.PrologError('type_error(callable)', goal);
|
|
230
|
+
}
|
|
231
|
+
return goal;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isUseModuleGoal(goal) {
|
|
235
|
+
return goal.type === 'compound' && goal.name === 'use_module' && [1, 2].includes(goal.arity);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function consultDesignations(engine, goal) {
|
|
239
|
+
if (goal.type === 'atom' && goal.name === '[]') return [];
|
|
240
|
+
if (goal.type !== 'compound' || goal.name !== '.' || goal.arity !== 2) return null;
|
|
241
|
+
const items = engine.properListItems(goal, new engine.Env());
|
|
242
|
+
if (items == null) return null;
|
|
243
|
+
return items.map((item) => {
|
|
244
|
+
if (item.type === 'var') throw new engine.PrologError('instantiation_error');
|
|
245
|
+
if (item.type !== 'atom') throw new engine.PrologError('type_error(atom)', item);
|
|
246
|
+
return item.name;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function readSource(designation) {
|
|
251
|
+
let filename = path.resolve(designation);
|
|
252
|
+
try {
|
|
253
|
+
await fs.access(filename);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (path.extname(filename)) throw error;
|
|
256
|
+
filename += '.pl';
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
text: await fs.readFile(filename, 'utf8'),
|
|
260
|
+
filename: path.basename(filename),
|
|
261
|
+
baseDir: path.dirname(filename),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function solveQuery(engine, state, goal, reader, output) {
|
|
266
|
+
const variables = queryVariables(goal);
|
|
267
|
+
const solver = state.solver;
|
|
268
|
+
solver.solutionsSeen = 0;
|
|
269
|
+
const solutions = solver.solve([goal], new engine.Env(), 0);
|
|
270
|
+
let current = pullSolution(solver, solutions);
|
|
271
|
+
if (current.error) {
|
|
272
|
+
if (current.error?.name === 'HaltSignal') return { halted: true, code: current.error.code };
|
|
273
|
+
throw current.error;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (current.result.done) {
|
|
277
|
+
output.write(' false.\n');
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let automatic = 0;
|
|
282
|
+
let firstAnswer = true;
|
|
283
|
+
while (!current.result.done) {
|
|
284
|
+
const next = pullSolution(solver, solutions);
|
|
285
|
+
output.write(current.output);
|
|
286
|
+
output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
287
|
+
firstAnswer = false;
|
|
288
|
+
if (!next.error && next.result.done) {
|
|
289
|
+
output.write('.\n');
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (automatic > 0 || automatic === Infinity) {
|
|
294
|
+
if (automatic !== Infinity) automatic--;
|
|
295
|
+
output.write('\n; ');
|
|
296
|
+
} else {
|
|
297
|
+
while (true) {
|
|
298
|
+
const controlLine = await reader.readControl('\n; ');
|
|
299
|
+
if (controlLine == null || controlLine === '' || controlLine === '\r' || controlLine === '\n' ||
|
|
300
|
+
controlLine.trimStart().startsWith('.')) {
|
|
301
|
+
if (typeof solutions.return === 'function') solutions.return();
|
|
302
|
+
output.write('... .\n');
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const control = controlLine === ' ' ? ' ' : controlLine.trimStart()[0];
|
|
306
|
+
if (control === ';' || control === 'n' || control === ' ') break;
|
|
307
|
+
if (control === 'a') {
|
|
308
|
+
automatic = Infinity;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (control === 'f') {
|
|
312
|
+
automatic = 4;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (control === 'w' || control === 'p') {
|
|
316
|
+
output.write(`${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (control === 'h') {
|
|
320
|
+
output.write(ANSWER_HELP);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
output.write('Action? ');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (next.error) {
|
|
328
|
+
output.write(next.output);
|
|
329
|
+
if (next.error?.name === 'HaltSignal') return { halted: true, code: next.error.code };
|
|
330
|
+
throw next.error;
|
|
331
|
+
}
|
|
332
|
+
current = next;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function pullSolution(solver, solutions) {
|
|
338
|
+
const stream = solver.io.resolve('user_output');
|
|
339
|
+
const originalWrite = stream?.write;
|
|
340
|
+
let captured = '';
|
|
341
|
+
if (stream) stream.write = (text) => { captured += String(text); };
|
|
342
|
+
try {
|
|
343
|
+
return { result: solutions.next(), output: captured };
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return { error, output: captured };
|
|
346
|
+
} finally {
|
|
347
|
+
if (stream) stream.write = originalWrite;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function queryVariables(goal) {
|
|
352
|
+
const variables = [];
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
const stack = [goal];
|
|
355
|
+
while (stack.length) {
|
|
356
|
+
const term = stack.pop();
|
|
357
|
+
if (term.type === 'var') {
|
|
358
|
+
if (!term.name.startsWith('__anon') && !seen.has(term.name)) {
|
|
359
|
+
seen.add(term.name);
|
|
360
|
+
variables.push(term);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
for (let i = term.args.length - 1; i >= 0; i--) stack.push(term.args[i]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return variables;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function formatAnswer(engine, state, variables, env) {
|
|
370
|
+
const bindings = [];
|
|
371
|
+
const names = new Map(variables.map((variable) => [variable.name, variable.name]));
|
|
372
|
+
let generated = 0;
|
|
373
|
+
|
|
374
|
+
for (const variable of variables) collectUnboundVariables(engine, variable, env, names, () => `_${letterName(generated++)}`);
|
|
375
|
+
for (const variable of variables) {
|
|
376
|
+
const value = engine.deref(variable, env);
|
|
377
|
+
if (value.type === 'var' && value.name === variable.name) continue;
|
|
378
|
+
bindings.push(`${variable.name} = ${engine.formatTermForWrite(value, env, {
|
|
379
|
+
quoted: true,
|
|
380
|
+
operators: [...state.program.operators.values()],
|
|
381
|
+
variableNames: names,
|
|
382
|
+
})}`);
|
|
383
|
+
}
|
|
384
|
+
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function collectUnboundVariables(engine, term, env, names, nextName) {
|
|
388
|
+
const stack = [term];
|
|
389
|
+
const seen = new Set();
|
|
390
|
+
while (stack.length) {
|
|
391
|
+
const current = engine.deref(stack.pop(), env);
|
|
392
|
+
if (current.type === 'var') {
|
|
393
|
+
if (!seen.has(current.name)) {
|
|
394
|
+
seen.add(current.name);
|
|
395
|
+
if (!names.has(current.name)) names.set(current.name, nextName());
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
for (let i = current.args.length - 1; i >= 0; i--) stack.push(current.args[i]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function letterName(index) {
|
|
404
|
+
const letter = String.fromCharCode(65 + (index % 26));
|
|
405
|
+
const suffix = Math.floor(index / 26);
|
|
406
|
+
return suffix === 0 ? letter : `${letter}${suffix}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function formatError(error) {
|
|
410
|
+
const message = error?.message ?? String(error);
|
|
411
|
+
return message.endsWith('.') ? message : `${message}.`;
|
|
412
|
+
}
|
|
@@ -19,7 +19,7 @@ case totals.
|
|
|
19
19
|
| 8.17 flags and hooks | required flags, mutation permissions, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases |
|
|
20
20
|
| Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power` |
|
|
21
21
|
| ISO/IEC 13211-2 modules | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
|
|
22
|
-
| ISO/IEC TS 13211-3 grammar rules | `-->`, terminal sequences, grammar control constructs, semicontexts, nonterminal indicators, `phrase/2-3
|
|
22
|
+
| ISO/IEC TS 13211-3 grammar rules | `-->`, terminal and partial sequences, grammar control constructs, semicontexts, nonterminal indicators, modules, `phrase/2-3`, steadfastness and errors | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_partial_sequences`, `dcg_phrase_steadfastness`, `dcg_dynamic_nonterminal_indicator`, `logtalk_dcg_phrase_identity`, `logtalk_dcg_semicontexts`, DCG error and precedence cases |
|
|
23
23
|
|
|
24
24
|
Corrigendum-specific coverage includes double-quoted atom operator priority
|
|
25
25
|
(Cor.1); the added predicates and evaluable functors, bar-operator rules,
|
|
@@ -90,11 +90,11 @@ ISO/IEC 13211-1 clauses 7 and 8. In particular, isolated negative cases keep
|
|
|
90
90
|
instantiation, type, domain, permission, representation, and evaluation errors
|
|
91
91
|
independently observable.
|
|
92
92
|
|
|
93
|
-
Selected cases are adapted from the ISO and standard-core suites of
|
|
94
|
-
Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
93
|
+
Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
94
|
+
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
95
95
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
96
96
|
|
|
97
|
-
The corpus has
|
|
97
|
+
The corpus has 373 cases in `iso/` and 779 file-based conformance cases in
|
|
98
98
|
total. The generated `conformance-report.md` is the authoritative source for
|
|
99
99
|
current category totals. Together with regression, documentation-sync, API,
|
|
100
100
|
example, and book-example checks, `npm test` is the release gate.
|
|
@@ -9,6 +9,13 @@ identifies its upstream test names. The cases were rewritten for EyeProlog's
|
|
|
9
9
|
file-based query and exact-output harness and are therefore modified from the
|
|
10
10
|
upstream files.
|
|
11
11
|
|
|
12
|
+
Part 3 grammar cases are additionally adapted from Logtalk's
|
|
13
|
+
`tests/logtalk/methods/phrase_2_3/tests.lgt` and
|
|
14
|
+
`tests/logtalk/dcgs/tests.lgt` suites. Object and unit-test scaffolding was
|
|
15
|
+
removed, translator-only assertions were converted to executable grammar
|
|
16
|
+
behavior where possible, and expected list errors follow the Part 3
|
|
17
|
+
`terminal_sequence` terminology used by EyeProlog.
|
|
18
|
+
|
|
12
19
|
Copyright 1998-2026 Paulo Moura <pmoura@logtalk.org>
|
|
13
20
|
|
|
14
21
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
% phrase/2 expands dynamically constructed grammar bodies using static semantics.
|
|
2
|
+
%% goal: dynamic_body(X0, X1)
|
|
3
|
+
|
|
4
|
+
dynamic_body(sequence, Tokens) :-
|
|
5
|
+
Body = ([a], [b]),
|
|
6
|
+
phrase(Body, Tokens).
|
|
7
|
+
dynamic_body(choice, Tokens) :-
|
|
8
|
+
Body = ([a]; [b]),
|
|
9
|
+
phrase(Body, Tokens).
|
|
10
|
+
dynamic_body(embedded, Tokens) :-
|
|
11
|
+
Body = ({true}, [ok]),
|
|
12
|
+
phrase(Body, Tokens).
|
|
13
|
+
dynamic_body(conditional, Tokens) :-
|
|
14
|
+
Body = ([x] -> [yes]; [no]),
|
|
15
|
+
phrase(Body, Tokens).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
% Part 3 phrase/3 accepts partial terminal sequences and preserves their tail.
|
|
2
|
+
%% goal: partial_sequence(X0)
|
|
3
|
+
|
|
4
|
+
partial_sequence(one) :-
|
|
5
|
+
phrase([a], [a|Tail], Tail),
|
|
6
|
+
Tail = [rest].
|
|
7
|
+
partial_sequence(two) :-
|
|
8
|
+
phrase([a, b], [a, b|Tail], Tail),
|
|
9
|
+
Tail = [rest].
|
|
10
|
+
partial_sequence(empty) :-
|
|
11
|
+
phrase([], Tail, Tail),
|
|
12
|
+
Tail = [rest].
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
% phrase/3 is steadfast in its third argument.
|
|
2
|
+
%% goal: steadfast(X0)
|
|
3
|
+
|
|
4
|
+
steadfast(bound_before) :-
|
|
5
|
+
Rest = [b],
|
|
6
|
+
phrase([a], [a, b], Rest).
|
|
7
|
+
steadfast(bound_after) :-
|
|
8
|
+
phrase([a], [a, b], Rest),
|
|
9
|
+
Rest = [b].
|
|
10
|
+
steadfast(selects_matching_alternative) :-
|
|
11
|
+
phrase(([a]; [a, b]), [a, b], [b]).
|
|
12
|
+
steadfast(shared_sequence) :-
|
|
13
|
+
phrase([], Sequence, Sequence),
|
|
14
|
+
Sequence = [same].
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
% Qualified nonterminals retain module context through nested phrase//1 calls.
|
|
2
|
+
:- module(dcg_chain, [leaf//0, branch//0, wrapper//0, answer/1]).
|
|
3
|
+
|
|
4
|
+
leaf --> [x].
|
|
5
|
+
branch --> dcg_chain:leaf.
|
|
6
|
+
wrapper --> phrase(dcg_chain:branch).
|
|
7
|
+
answer(Tokens) :- phrase(wrapper, Tokens).
|
|
8
|
+
|
|
9
|
+
%% goal: dcg_chain:answer(Tokens)
|
|
10
|
+
%% goal: phrase(dcg_chain:wrapper, Tokens)
|