eyeprolog 1.1.0 → 1.1.1
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 -2
- package/conformance-report.md +2 -2
- package/examples/book/README.md +2 -1
- package/examples/book/chapter-22/04-sentence.pl +10 -14
- package/examples/book/chapter-38/02-look_ahead.pl +2 -0
- package/examples/dcg-command-parser.pl +26 -0
- package/examples/output/dcg-command-parser.pl +4 -0
- package/package.json +1 -1
- package/playground.html +1 -0
- package/src/dcg.js +241 -0
- package/src/iso.js +42 -0
- package/src/parser.js +23 -2
- package/src/program.js +22 -5
- package/src/solver.js +6 -2
- package/test/conformance/ISO-MATRIX.md +3 -1
- package/test/conformance/README.md +6 -4
- package/test/conformance/cases/iso/cut_disjunction_scope.pl +5 -0
- package/test/conformance/cases/iso/dcg_call_phrase_semicontext.pl +12 -0
- package/test/conformance/cases/iso/dcg_control_constructs.pl +12 -0
- package/test/conformance/cases/iso/dcg_module_nonterminal_indicator.pl +8 -0
- package/test/conformance/cases/iso/dcg_terminals_and_remainder.pl +8 -0
- package/test/conformance/errors/iso/dcg_phrase_bad_input.pl +1 -0
- package/test/conformance/errors/iso/dcg_phrase_bad_output.pl +1 -0
- package/test/conformance/errors/iso/dcg_phrase_noncallable.pl +1 -0
- package/test/conformance/errors/iso/dcg_phrase_variable.pl +1 -0
- package/test/conformance/expected/iso/cut_disjunction_scope.pl +2 -0
- package/test/conformance/expected/iso/dcg_call_phrase_semicontext.pl +4 -0
- package/test/conformance/expected/iso/dcg_control_constructs.pl +6 -0
- package/test/conformance/expected/iso/dcg_module_nonterminal_indicator.pl +2 -0
- package/test/conformance/expected/iso/dcg_terminals_and_remainder.pl +2 -0
- package/test/conformance/expected-errors/iso/dcg_phrase_bad_input.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_phrase_bad_output.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_phrase_noncallable.txt +1 -0
- package/test/conformance/expected-errors/iso/dcg_phrase_variable.txt +1 -0
- package/test/run-regression.mjs +24 -2
- package/the-art-of-eyeprolog.md +89 -39
- package/tools/extract-book-examples.mjs +1 -1
- package/why-eyeprolog.md +5 -1
package/README.md
CHANGED
|
@@ -33,9 +33,19 @@ Programs may declare their default queries with `%% goal:` comments.
|
|
|
33
33
|
Double-quoted text follows the ISO `double_quotes` flag and defaults to a
|
|
34
34
|
proper list of one-character atoms (`chars`), matching Trealla and Scryer.
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## ISO modules and definite clause grammars
|
|
37
37
|
|
|
38
|
-
EyeProlog
|
|
38
|
+
EyeProlog implements ISO/IEC 13211-2 modules and the grammar rules and
|
|
39
|
+
`phrase/2-3` predicates of ISO/IEC TS 13211-3:2025. For example:
|
|
40
|
+
|
|
41
|
+
```prolog
|
|
42
|
+
sentence --> [hello], noun.
|
|
43
|
+
noun --> [world] | [prolog].
|
|
44
|
+
|
|
45
|
+
%% goal: phrase(sentence, Words)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
EyeProlog also adds 44 public library predicates to its 129-entry ISO registry.
|
|
39
49
|
**All 44 are ordinary Prolog clauses** in `src/lib/eyeprolog.pl` and
|
|
40
50
|
`src/lib/lists.pl`. They are ISO/IEC 13211-2 modules, loaded explicitly with
|
|
41
51
|
`use_module(library(eyeprolog))` or `use_module(library(lists))`, following the
|
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 | 135 | 178 | 0 | 0 | 313 |
|
|
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** | **450** | **229** | **19** | **21** | **719** |
|
package/examples/book/README.md
CHANGED
|
@@ -127,7 +127,7 @@ npm run generate
|
|
|
127
127
|
- [01-tree.pl](chapter-22/01-tree.pl)
|
|
128
128
|
- [02-tree_member.pl](chapter-22/02-tree_member.pl)
|
|
129
129
|
- [03-mirror.pl](chapter-22/03-mirror.pl) — Transforming a tree
|
|
130
|
-
- [04-sentence.pl](chapter-22/04-sentence.pl) — A
|
|
130
|
+
- [04-sentence.pl](chapter-22/04-sentence.pl) — A standard definite clause grammar
|
|
131
131
|
- [05-evaluate.pl](chapter-22/05-evaluate.pl) — Interpreting an expression
|
|
132
132
|
- [06-lookup.pl](chapter-22/06-lookup.pl)
|
|
133
133
|
- [07-simplify.pl](chapter-22/07-simplify.pl) — Rewriting symbolic expressions
|
|
@@ -233,6 +233,7 @@ npm run generate
|
|
|
233
233
|
## Chapter 38: Language and ISO profile
|
|
234
234
|
|
|
235
235
|
- [01-city.pl](chapter-38/01-city.pl)
|
|
236
|
+
- [02-look_ahead.pl](chapter-38/02-look_ahead.pl) — ISO Part 3 definite clause grammars
|
|
236
237
|
|
|
237
238
|
## Chapter 39: Built-in predicates by programming role
|
|
238
239
|
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
% From The Art of EyeProlog, Chapter 22 — A
|
|
2
|
-
sentence
|
|
3
|
-
noun_phrase(Input, AfterNoun),
|
|
4
|
-
verb_phrase(AfterNoun, Rest).
|
|
1
|
+
% From The Art of EyeProlog, Chapter 22 — A standard definite clause grammar.
|
|
2
|
+
sentence --> noun_phrase, verb_phrase.
|
|
5
3
|
|
|
6
|
-
noun_phrase
|
|
7
|
-
noun_phrase
|
|
4
|
+
noun_phrase --> [the], noun.
|
|
5
|
+
noun_phrase --> [a], noun.
|
|
8
6
|
|
|
9
|
-
noun
|
|
10
|
-
noun
|
|
7
|
+
noun --> [robot].
|
|
8
|
+
noun --> [scientist].
|
|
11
9
|
|
|
12
|
-
verb_phrase
|
|
13
|
-
verb(Input, AfterVerb),
|
|
14
|
-
noun_phrase(AfterVerb, Rest).
|
|
10
|
+
verb_phrase --> verb, noun_phrase.
|
|
15
11
|
|
|
16
|
-
verb
|
|
17
|
-
verb
|
|
12
|
+
verb --> [helps].
|
|
13
|
+
verb --> [observes].
|
|
18
14
|
|
|
19
|
-
complete_sentence(Words) :- sentence
|
|
15
|
+
complete_sentence(Words) :- phrase(sentence, Words).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
% Parse and generate commands for a tiny home-automation language with a DCG.
|
|
2
|
+
%
|
|
3
|
+
% The grammar separates surface tokens from the structured command term used
|
|
4
|
+
% by an application. phrase/2 requires complete input, while phrase/3 leaves
|
|
5
|
+
% unconsumed tokens available to a surrounding parser.
|
|
6
|
+
%% goal: dcg_example(X0, X1)
|
|
7
|
+
|
|
8
|
+
command(set(light(Room), State)) -->
|
|
9
|
+
[set], room(Room), [light, to], state(State).
|
|
10
|
+
|
|
11
|
+
room(kitchen) --> [kitchen].
|
|
12
|
+
room(hall) --> [hall].
|
|
13
|
+
|
|
14
|
+
state(on) --> [on].
|
|
15
|
+
state(off) --> [off].
|
|
16
|
+
|
|
17
|
+
dcg_example(parsed, Command) :-
|
|
18
|
+
phrase(command(Command), [set, kitchen, light, to, on]).
|
|
19
|
+
dcg_example(generated, Tokens) :-
|
|
20
|
+
phrase(command(set(light(hall), off)), Tokens).
|
|
21
|
+
dcg_example(remainder, Rest) :-
|
|
22
|
+
phrase(command(set(light(kitchen), on)),
|
|
23
|
+
[set, kitchen, light, to, on, then, wait],
|
|
24
|
+
Rest).
|
|
25
|
+
dcg_example(rejected, invalid_command) :-
|
|
26
|
+
\+ phrase(command(_), [set, garage, light, to, blinking]).
|
package/package.json
CHANGED
package/playground.html
CHANGED
package/src/dcg.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// ISO/IEC TS 13211-3:2025 definite clause grammar expansion.
|
|
2
|
+
// Grammar rules are lowered to ordinary clauses during program preparation;
|
|
3
|
+
// phrase/2-3 use the same body expansion for dynamically supplied grammars.
|
|
4
|
+
import {
|
|
5
|
+
ATOM, COMPOUND, VAR, Env, atom, compound, deref, emptyList,
|
|
6
|
+
flattenConjunction, variable,
|
|
7
|
+
} from './term.js';
|
|
8
|
+
import { PrologError } from './iso.js';
|
|
9
|
+
|
|
10
|
+
let dcgFresh = 0;
|
|
11
|
+
|
|
12
|
+
function freshDcgVariable(label = 'S') {
|
|
13
|
+
// NUL cannot occur in a source variable name, so generated difference-list
|
|
14
|
+
// variables cannot capture a variable written by the program.
|
|
15
|
+
return variable(`\u0000dcg${++dcgFresh}:${label}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function conjunction(first, second) {
|
|
19
|
+
return compound(',', [first, second]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function equality(left, right) {
|
|
23
|
+
return compound('=', [left, right]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function listWithTail(items, tail) {
|
|
27
|
+
let list = tail;
|
|
28
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
29
|
+
list = compound('.', [items[index], list]);
|
|
30
|
+
}
|
|
31
|
+
return list;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function terminalItems(term, env = new Env()) {
|
|
35
|
+
const items = [];
|
|
36
|
+
const original = term;
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
let cursor = deref(term, env);
|
|
39
|
+
while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
|
|
40
|
+
if (seen.has(cursor)) throw new PrologError('type_error(terminal_sequence)', original);
|
|
41
|
+
seen.add(cursor);
|
|
42
|
+
items.push(cursor.args[0]);
|
|
43
|
+
cursor = deref(cursor.args[1], env);
|
|
44
|
+
}
|
|
45
|
+
if (cursor.type === ATOM && cursor.name === '[]') return items;
|
|
46
|
+
if (cursor.type === VAR) throw new PrologError('instantiation_error');
|
|
47
|
+
throw new PrologError('type_error(terminal_sequence)', original);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function terminalsGoal(terminals, input, output, env) {
|
|
51
|
+
return equality(input, listWithTail(terminalItems(terminals, env), output));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendStateArguments(nonterminal, input, output, module) {
|
|
55
|
+
nonterminal = deref(nonterminal, new Env());
|
|
56
|
+
if (nonterminal.type === ATOM) {
|
|
57
|
+
const goal = compound(nonterminal.name, [input, output]);
|
|
58
|
+
goal.module = module;
|
|
59
|
+
return goal;
|
|
60
|
+
}
|
|
61
|
+
if (nonterminal.type !== COMPOUND) throw new PrologError('type_error(callable)', nonterminal);
|
|
62
|
+
if (nonterminal.name === ':' && nonterminal.arity === 2) {
|
|
63
|
+
const qualifier = nonterminal.args[0];
|
|
64
|
+
if (qualifier.type === VAR) throw new PrologError('instantiation_error');
|
|
65
|
+
if (qualifier.type !== ATOM) throw new PrologError('type_error(atom)', qualifier);
|
|
66
|
+
return compound(':', [qualifier, appendStateArguments(nonterminal.args[1], input, output, qualifier.name)]);
|
|
67
|
+
}
|
|
68
|
+
const goal = compound(nonterminal.name, [...nonterminal.args, input, output]);
|
|
69
|
+
goal.module = nonterminal.module ?? module;
|
|
70
|
+
return goal;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function markGoalModule(term, module) {
|
|
74
|
+
if (!term || (term.type !== ATOM && term.type !== COMPOUND)) return term;
|
|
75
|
+
term.module ??= module;
|
|
76
|
+
if (term.type === COMPOUND &&
|
|
77
|
+
[',', ';', '->', '\\+', 'call', 'once', 'catch', 'phrase'].includes(term.name)) {
|
|
78
|
+
for (const argument of term.args) markGoalModule(argument, module);
|
|
79
|
+
}
|
|
80
|
+
return term;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isTerminalSequence(term) {
|
|
84
|
+
return (term.type === ATOM && term.name === '[]') ||
|
|
85
|
+
(term.type === COMPOUND && term.name === '.' && term.arity === 2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function expandDcgBody(body, input, output, options = {}) {
|
|
89
|
+
const env = options.env ?? new Env();
|
|
90
|
+
const module = options.module ?? 'user';
|
|
91
|
+
body = deref(body, env);
|
|
92
|
+
|
|
93
|
+
if (body.type === VAR) {
|
|
94
|
+
const goal = compound('phrase', [body, input, output]);
|
|
95
|
+
goal.module = module;
|
|
96
|
+
return goal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isTerminalSequence(body)) return terminalsGoal(body, input, output, env);
|
|
100
|
+
|
|
101
|
+
if (body.type === COMPOUND && body.name === ',' && body.arity === 2) {
|
|
102
|
+
const middle = freshDcgVariable('sequence');
|
|
103
|
+
return conjunction(
|
|
104
|
+
expandDcgBody(body.args[0], input, middle, options),
|
|
105
|
+
expandDcgBody(body.args[1], middle, output, options),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (body.type === COMPOUND && [';', '|'].includes(body.name) && body.arity === 2) {
|
|
110
|
+
const left = body.args[0];
|
|
111
|
+
if (body.name === ';' && left.type === COMPOUND && left.name === '->' && left.arity === 2) {
|
|
112
|
+
const middle = freshDcgVariable('condition');
|
|
113
|
+
return compound(';', [
|
|
114
|
+
compound('->', [
|
|
115
|
+
expandDcgBody(left.args[0], input, middle, options),
|
|
116
|
+
expandDcgBody(left.args[1], middle, output, options),
|
|
117
|
+
]),
|
|
118
|
+
expandDcgBody(body.args[1], input, output, options),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
return compound(';', [
|
|
122
|
+
expandDcgBody(left, input, output, options),
|
|
123
|
+
expandDcgBody(body.args[1], input, output, options),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (body.type === COMPOUND && body.name === '{}' && body.arity === 1) {
|
|
128
|
+
return conjunction(markGoalModule(body.args[0], module), equality(input, output));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (body.type === COMPOUND && body.name === 'call' && body.arity === 1) {
|
|
132
|
+
const closure = body.args[0];
|
|
133
|
+
if (closure.type === ATOM || closure.type === COMPOUND) closure.module ??= module;
|
|
134
|
+
const goal = compound('call', [closure, input, output]);
|
|
135
|
+
goal.module = module;
|
|
136
|
+
return goal;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (body.type === COMPOUND && body.name === 'phrase' && body.arity === 1) {
|
|
140
|
+
const goal = compound('phrase', [body.args[0], input, output]);
|
|
141
|
+
goal.module = module;
|
|
142
|
+
return goal;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (body.type === ATOM && body.name === '!') {
|
|
146
|
+
return conjunction(body, equality(input, output));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// TS 13211-3 leaves \+//1 and ->//2 implementation dependent. EyeProlog
|
|
150
|
+
// uses the widespread non-consuming negation and state-threading if-then
|
|
151
|
+
// definitions and documents these choices in its conformance profile.
|
|
152
|
+
if (body.type === COMPOUND && body.name === '\\+' && body.arity === 1) {
|
|
153
|
+
const ignored = freshDcgVariable('negated');
|
|
154
|
+
return conjunction(
|
|
155
|
+
compound('\\+', [expandDcgBody(body.args[0], input, ignored, options)]),
|
|
156
|
+
equality(input, output),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (body.type === COMPOUND && body.name === '->' && body.arity === 2) {
|
|
161
|
+
const middle = freshDcgVariable('condition');
|
|
162
|
+
return compound('->', [
|
|
163
|
+
expandDcgBody(body.args[0], input, middle, options),
|
|
164
|
+
expandDcgBody(body.args[1], middle, output, options),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (body.type !== ATOM && body.type !== COMPOUND) {
|
|
169
|
+
throw new PrologError('type_error(callable)', body);
|
|
170
|
+
}
|
|
171
|
+
return appendStateArguments(body, input, output, module);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function splitGrammarHead(head) {
|
|
175
|
+
let terminals = null;
|
|
176
|
+
if (head.type === COMPOUND && head.name === ',' && head.arity === 2) {
|
|
177
|
+
terminals = head.args[1];
|
|
178
|
+
head = head.args[0];
|
|
179
|
+
}
|
|
180
|
+
let module = null;
|
|
181
|
+
if (head.type === COMPOUND && head.name === ':' && head.arity === 2) {
|
|
182
|
+
if (head.args[0].type === VAR) throw new PrologError('instantiation_error');
|
|
183
|
+
if (head.args[0].type !== ATOM) throw new PrologError('type_error(atom)', head.args[0]);
|
|
184
|
+
module = head.args[0].name;
|
|
185
|
+
head = head.args[1];
|
|
186
|
+
}
|
|
187
|
+
if (head.type === VAR) throw new PrologError('instantiation_error');
|
|
188
|
+
if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
|
|
189
|
+
if (isTerminalSequence(head) ||
|
|
190
|
+
(head.type === COMPOUND && [',', ';', '|', '->', '{}'].includes(head.name)) ||
|
|
191
|
+
(head.type === ATOM && ['!', '{}'].includes(head.name))) {
|
|
192
|
+
throw new PrologError('permission_error(define, grammar_rule)', head);
|
|
193
|
+
}
|
|
194
|
+
return { head, terminals, module };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function expandDcgRuleClause(clause, defaultModule = 'user') {
|
|
198
|
+
if (clause?.body?.length !== 0 || clause?.head?.type !== COMPOUND ||
|
|
199
|
+
clause.head.name !== '-->' || clause.head.arity !== 2) return null;
|
|
200
|
+
|
|
201
|
+
const declaration = splitGrammarHead(clause.head.args[0]);
|
|
202
|
+
const module = declaration.module ?? defaultModule;
|
|
203
|
+
const input = freshDcgVariable('input');
|
|
204
|
+
const output = freshDcgVariable('output');
|
|
205
|
+
const expandedHead = appendStateArguments(declaration.head, input, output, module);
|
|
206
|
+
delete expandedHead.module;
|
|
207
|
+
|
|
208
|
+
let bodyOutput = output;
|
|
209
|
+
if (declaration.terminals != null) bodyOutput = freshDcgVariable('semicontext');
|
|
210
|
+
let expandedBody = expandDcgBody(clause.head.args[1], input, bodyOutput, { module });
|
|
211
|
+
if (declaration.terminals != null) {
|
|
212
|
+
expandedBody = conjunction(
|
|
213
|
+
expandedBody,
|
|
214
|
+
terminalsGoal(declaration.terminals, output, bodyOutput, new Env()),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const expanded = {
|
|
219
|
+
head: expandedHead,
|
|
220
|
+
body: flattenConjunction(expandedBody),
|
|
221
|
+
module,
|
|
222
|
+
grammarRule: clause.head,
|
|
223
|
+
};
|
|
224
|
+
if (clause.source) expanded.source = clause.source;
|
|
225
|
+
return expanded;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function isListOrPartialList(term, env) {
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
let cursor = deref(term, env);
|
|
231
|
+
while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
|
|
232
|
+
if (seen.has(cursor)) return false;
|
|
233
|
+
seen.add(cursor);
|
|
234
|
+
cursor = deref(cursor.args[1], env);
|
|
235
|
+
}
|
|
236
|
+
return cursor.type === VAR || (cursor.type === ATOM && cursor.name === '[]');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function emptyTerminalSequence() {
|
|
240
|
+
return emptyList();
|
|
241
|
+
}
|
package/src/iso.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { createParserOperatorState, parseClauses } from './parser.js';
|
|
9
9
|
import { formatTermForWrite } from './write.js';
|
|
10
|
+
import { emptyTerminalSequence, expandDcgBody, isListOrPartialList } from './dcg.js';
|
|
10
11
|
|
|
11
12
|
let isoFresh = 0;
|
|
12
13
|
|
|
@@ -150,6 +151,8 @@ export const isoBuiltins = {
|
|
|
150
151
|
registry.add('repeat', 0, repeatBuiltin);
|
|
151
152
|
registry.add(';', 2, disjunctionBuiltin);
|
|
152
153
|
registry.add('->', 2, ifThenBuiltin);
|
|
154
|
+
registry.add('phrase', 2, phraseBuiltin);
|
|
155
|
+
registry.add('phrase', 3, phraseBuiltin);
|
|
153
156
|
|
|
154
157
|
registry.add('is', 2, isBuiltin, { deterministic: true });
|
|
155
158
|
registry.add('=:=', 2, arithmeticComparison((n) => n === 0), { deterministic: true });
|
|
@@ -1545,6 +1548,38 @@ function* callClosureBuiltin({ solver, goal, env }) {
|
|
|
1545
1548
|
solver.absorbStatsFrom(child);
|
|
1546
1549
|
}
|
|
1547
1550
|
}
|
|
1551
|
+
|
|
1552
|
+
function* phraseBuiltin({ solver, goal, env }) {
|
|
1553
|
+
const grammarBody = deref(goal.args[0], env);
|
|
1554
|
+
if (grammarBody.type === VAR) throw new PrologError('instantiation_error');
|
|
1555
|
+
if (grammarBody.type !== ATOM && grammarBody.type !== COMPOUND) {
|
|
1556
|
+
throw new PrologError('type_error(callable)', grammarBody);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const input = goal.args[1];
|
|
1560
|
+
const requestedOutput = goal.arity === 2 ? emptyTerminalSequence() : goal.args[2];
|
|
1561
|
+
if (!isListOrPartialList(input, env)) {
|
|
1562
|
+
throw new PrologError('type_error(terminal_sequence)', deref(input, env));
|
|
1563
|
+
}
|
|
1564
|
+
if (!isListOrPartialList(requestedOutput, env)) {
|
|
1565
|
+
throw new PrologError('type_error(terminal_sequence)', deref(requestedOutput, env));
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Delay the final output unification to keep phrase/3 steadfast in its
|
|
1569
|
+
// third argument, as required by the Part 3 execution model.
|
|
1570
|
+
const finalOutput = variable(`\u0000phrase:${++isoFresh}`);
|
|
1571
|
+
const expanded = expandDcgBody(grammarBody, input, finalOutput, {
|
|
1572
|
+
env,
|
|
1573
|
+
module: goal.module ?? grammarBody.module ?? 'user',
|
|
1574
|
+
});
|
|
1575
|
+
const finish = compound('=', [finalOutput, requestedOutput]);
|
|
1576
|
+
const child = solver.cloneForInnerGoal();
|
|
1577
|
+
try {
|
|
1578
|
+
yield* child.solve([expanded, finish], env, 0);
|
|
1579
|
+
} finally {
|
|
1580
|
+
solver.absorbStatsFrom(child);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1548
1583
|
function formalErrorTerm(error) {
|
|
1549
1584
|
const parse = (text) => {
|
|
1550
1585
|
const open = text.indexOf('(');
|
|
@@ -1625,7 +1660,14 @@ function* disjunctionBuiltin({ solver, goal, env }) {
|
|
|
1625
1660
|
yield* solver.solve([callable(goal.args[1], env)], env.clone(), 0);
|
|
1626
1661
|
return;
|
|
1627
1662
|
}
|
|
1663
|
+
const marker = solver.active[solver.active.length - 1] ?? null;
|
|
1664
|
+
const markerCutEpoch = marker?.cutEpoch ?? 0;
|
|
1665
|
+
const solverCutEpoch = solver.cutEpoch;
|
|
1628
1666
|
yield* solver.solve([callable(goal.args[0], env)], env.clone(), 0);
|
|
1667
|
+
const cutThisScope = marker == null
|
|
1668
|
+
? solver.cutEpoch !== solverCutEpoch
|
|
1669
|
+
: (marker.cutEpoch ?? 0) !== markerCutEpoch;
|
|
1670
|
+
if (cutThisScope) return;
|
|
1629
1671
|
yield* solver.solve([callable(goal.args[1], env)], env.clone(), 0);
|
|
1630
1672
|
}
|
|
1631
1673
|
function* ifThenBuiltin({ solver, goal, env }) {
|
package/src/parser.js
CHANGED
|
@@ -39,6 +39,8 @@ const graphicAtomChars = '#$&*+-./<=>@^~\\;:';
|
|
|
39
39
|
// canonical notation. Commas remain separators except inside parentheses.
|
|
40
40
|
const INFIX_OPERATORS = new Map([
|
|
41
41
|
[':-', { precedence: 2, associativity: 'none' }],
|
|
42
|
+
['-->', { precedence: 2, associativity: 'none' }],
|
|
43
|
+
['|', { precedence: 4, associativity: 'right' }],
|
|
42
44
|
[';', { precedence: 5, associativity: 'right' }],
|
|
43
45
|
['->', { precedence: 7, associativity: 'right' }],
|
|
44
46
|
[',', { precedence: 10, associativity: 'right' }],
|
|
@@ -82,7 +84,8 @@ const PREFIX_OPERATORS = new Map([
|
|
|
82
84
|
]);
|
|
83
85
|
|
|
84
86
|
export const ISO_OPERATOR_DEFINITIONS = [
|
|
85
|
-
[1200, 'xfx', ':-'], [1200, 'fx', ':-'],
|
|
87
|
+
[1200, 'xfx', ':-'], [1200, 'fx', ':-'], [1200, 'xfx', '-->'],
|
|
88
|
+
[1105, 'xfy', '|'],
|
|
86
89
|
[1100, 'xfy', ';'], [1050, 'xfy', '->'], [1000, 'xfy', ','],
|
|
87
90
|
[900, 'fy', '\\+'],
|
|
88
91
|
...['=', '=..', '\\=', '==', '\\==', '@<', '@=<', '@>', '@>=', 'is',
|
|
@@ -618,7 +621,25 @@ class Parser {
|
|
|
618
621
|
accept(clause);
|
|
619
622
|
continue;
|
|
620
623
|
}
|
|
621
|
-
|
|
624
|
+
let head = this.parseTerm(3);
|
|
625
|
+
// ISO/IEC TS 13211-3 grammar rules use -->/2 at the same top-level
|
|
626
|
+
// priority as clauses. The left side may include an unparenthesized
|
|
627
|
+
// semicontext: NonTerminal, Terminals --> Body.
|
|
628
|
+
if (this.token.type === TOK.COMMA) {
|
|
629
|
+
this.advance();
|
|
630
|
+
head = compound(',', [head, this.parseTerm(3)]);
|
|
631
|
+
}
|
|
632
|
+
if (this.operatorTokenName() === '-->') {
|
|
633
|
+
this.advance();
|
|
634
|
+
const grammarBody = this.parseTerm(0, true);
|
|
635
|
+
this.expect(TOK.DOT, '.');
|
|
636
|
+
this.advance();
|
|
637
|
+
const clause = { head: compound('-->', [head, grammarBody]), body: [] };
|
|
638
|
+
clauseNumber++;
|
|
639
|
+
if (this.sourceMetadata) clause.source = { filename: this.filename, line, clause: clauseNumber };
|
|
640
|
+
accept(clause);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
622
643
|
const body = [];
|
|
623
644
|
if (this.token.type === TOK.IF) {
|
|
624
645
|
this.advance();
|
package/src/program.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { PrologError } from './iso.js';
|
|
12
12
|
import { currentWorkingDirectory, fs, path } from './platform.js';
|
|
13
13
|
import { standardLibrarySources } from './standard-library.js';
|
|
14
|
+
import { expandDcgRuleClause } from './dcg.js';
|
|
14
15
|
|
|
15
16
|
const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
|
|
16
17
|
const FAST_PARSE_ABORT = Symbol('fastParseAbort');
|
|
@@ -663,6 +664,8 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
663
664
|
batch.length = 0;
|
|
664
665
|
};
|
|
665
666
|
const accept = (clause) => {
|
|
667
|
+
const grammarClause = expandDcgRuleClause(clause, context.module);
|
|
668
|
+
if (grammarClause) clause = grammarClause;
|
|
666
669
|
const moduleDeclaration = moduleDirective(clause);
|
|
667
670
|
if (moduleDeclaration) {
|
|
668
671
|
flush();
|
|
@@ -690,9 +693,9 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
690
693
|
}
|
|
691
694
|
const include = includeDirective(clause);
|
|
692
695
|
if (!include) {
|
|
693
|
-
clause.module
|
|
696
|
+
clause.module ??= context.module;
|
|
694
697
|
if (!isDirectiveClause(clause)) {
|
|
695
|
-
for (const goal of clause.body) annotateGoalModule(goal,
|
|
698
|
+
for (const goal of clause.body) annotateGoalModule(goal, clause.module);
|
|
696
699
|
}
|
|
697
700
|
batch.push(clause);
|
|
698
701
|
if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
|
|
@@ -759,9 +762,14 @@ function moduleExportIndicators(term) {
|
|
|
759
762
|
if (items == null) return null;
|
|
760
763
|
const indicators = [];
|
|
761
764
|
for (const item of items) {
|
|
762
|
-
if (item.type !== COMPOUND || item.name
|
|
765
|
+
if (item.type !== COMPOUND || !['/', '//'].includes(item.name) || item.arity !== 2) return null;
|
|
763
766
|
const indicator = predicateIndicator(item.args[0], item.args[1]);
|
|
764
767
|
if (!indicator) return null;
|
|
768
|
+
if (item.name === '//') {
|
|
769
|
+
indicator.arity += 2;
|
|
770
|
+
indicator.key = `${indicator.name}/${indicator.arity}`;
|
|
771
|
+
indicator.nonterminalArity = indicator.arity - 2;
|
|
772
|
+
}
|
|
765
773
|
indicators.push(indicator);
|
|
766
774
|
}
|
|
767
775
|
return indicators;
|
|
@@ -844,12 +852,21 @@ function dynamicDirectiveIndicators(clause) {
|
|
|
844
852
|
if (directive.type !== COMPOUND || directive.name !== 'dynamic' || directive.arity !== 1) return [];
|
|
845
853
|
const terms = properListItems(directive.args[0], new Env()) ?? flattenDirectiveSequence(directive.args[0]);
|
|
846
854
|
return terms.map((indicator) =>
|
|
847
|
-
indicator.type === COMPOUND && indicator.name
|
|
848
|
-
?
|
|
855
|
+
indicator.type === COMPOUND && ['/', '//'].includes(indicator.name) && indicator.arity === 2
|
|
856
|
+
? nonterminalOrPredicateIndicator(indicator)
|
|
849
857
|
: null
|
|
850
858
|
).filter(Boolean);
|
|
851
859
|
}
|
|
852
860
|
|
|
861
|
+
function nonterminalOrPredicateIndicator(term) {
|
|
862
|
+
const indicator = predicateIndicator(term.args[0], term.args[1]);
|
|
863
|
+
if (!indicator || term.name !== '//') return indicator;
|
|
864
|
+
indicator.arity += 2;
|
|
865
|
+
indicator.key = `${indicator.name}/${indicator.arity}`;
|
|
866
|
+
indicator.nonterminalArity = indicator.arity - 2;
|
|
867
|
+
return indicator;
|
|
868
|
+
}
|
|
869
|
+
|
|
853
870
|
function flattenDirectiveSequence(term) {
|
|
854
871
|
if (term.type === COMPOUND && term.name === ',' && term.arity === 2) {
|
|
855
872
|
return [...flattenDirectiveSequence(term.args[0]), ...flattenDirectiveSequence(term.args[1])];
|
package/src/solver.js
CHANGED
|
@@ -61,6 +61,7 @@ export class Solver {
|
|
|
61
61
|
this.io = options.io ?? new StreamManager(options.ioOptions);
|
|
62
62
|
this.solveStacks = [];
|
|
63
63
|
this.active = [];
|
|
64
|
+
this.cutEpoch = 0;
|
|
64
65
|
this.memo = new Map();
|
|
65
66
|
this.tableCoordinator = null;
|
|
66
67
|
this.groundChainSuccess = new Set();
|
|
@@ -215,10 +216,12 @@ export class Solver {
|
|
|
215
216
|
// be run early as pure filters. Stop at internal sentinels so rule-body
|
|
216
217
|
// active guards are released before the caller's remaining goals are seen.
|
|
217
218
|
const selectedIndex = selectReadyDeterministicBuiltin(goals, env, this.registry);
|
|
218
|
-
const goal = goals[selectedIndex];
|
|
219
|
+
const goal = deref(goals[selectedIndex], env);
|
|
219
220
|
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
220
221
|
if (goal.type === 'atom' && goal.name === '!' && goal.arity === 0) {
|
|
221
222
|
const marker = active[active.length - 1] ?? null;
|
|
223
|
+
this.cutEpoch++;
|
|
224
|
+
if (marker) marker.cutEpoch = (marker.cutEpoch ?? 0) + 1;
|
|
222
225
|
for (const solveStack of this.solveStacks) {
|
|
223
226
|
for (let i = solveStack.length - 1; i >= 0; i--) {
|
|
224
227
|
if (marker == null || solveStack[i].active?.includes(marker)) solveStack.splice(i, 1);
|
|
@@ -247,7 +250,9 @@ export class Solver {
|
|
|
247
250
|
continue;
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
if (goal.type === 'var') throw new PrologError('instantiation_error');
|
|
250
254
|
const callable = goal.type === COMPOUND || goal.type === 'atom';
|
|
255
|
+
if (!callable) throw new PrologError('type_error(callable)', goal);
|
|
251
256
|
const def = callable ? this.registry.get(goal.name, goal.arity) : null;
|
|
252
257
|
this.active = active;
|
|
253
258
|
if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
|
|
@@ -274,7 +279,6 @@ export class Solver {
|
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
this.stats.solve_one_goal_calls++;
|
|
277
|
-
if (!callable) break;
|
|
278
282
|
const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
279
283
|
if (!group) {
|
|
280
284
|
if (this.prologFlags.get('unknown')?.value?.name === 'error') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ISO
|
|
1
|
+
# ISO Prolog conformance matrix
|
|
2
2
|
|
|
3
3
|
The normative baseline for the `iso/` corpus is ISO/IEC 13211-1:1995 plus
|
|
4
4
|
Technical Corrigenda 1:2007, 2:2012, and 3:2017. This matrix links the standard
|
|
@@ -18,6 +18,8 @@ case totals.
|
|
|
18
18
|
| 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, Logtalk-derived error cases |
|
|
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
|
+
| 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` | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_call_phrase_semicontext`, DCG error cases |
|
|
21
23
|
|
|
22
24
|
Corrigendum-specific coverage includes double-quoted atom operator priority
|
|
23
25
|
(Cor.1); the added predicates and evaluable functors, bar-operator rules,
|
|
@@ -7,13 +7,15 @@ supported ISO Prolog profile, built-ins, extensions, and reasoner behavior.
|
|
|
7
7
|
|
|
8
8
|
The suite is intentionally file-based. Exact standard output, errors, warnings,
|
|
9
9
|
and proof output test the behavior of the JavaScript implementation.
|
|
10
|
-
[ISO-MATRIX.md](ISO-MATRIX.md) maps normative clause families
|
|
11
|
-
corrigenda to representative
|
|
10
|
+
[ISO-MATRIX.md](ISO-MATRIX.md) maps Part 1 normative clause families, all three
|
|
11
|
+
corrigenda, Part 2 modules, and Part 3 definite clause grammars to representative
|
|
12
|
+
executable cases.
|
|
12
13
|
|
|
13
14
|
“Conformance” here means conformance to EyeProlog's documented ISO compatibility
|
|
14
15
|
profile and implementation extensions. The default registry covers the exact
|
|
15
16
|
predicate indicators listed in Appendix B of the book across the supported
|
|
16
|
-
ISO/IEC 13211-1:1995
|
|
17
|
+
ISO/IEC 13211-1:1995, ISO/IEC 13211-2:2000, and ISO/IEC TS 13211-3:2025
|
|
18
|
+
standard families. This suite is still not an independent certification of
|
|
17
19
|
every processor requirement, lexical edge, option combination, or prescribed
|
|
18
20
|
error precedence. Cases under `iso/` identify standards-derived behavior;
|
|
19
21
|
other directories cover EyeProlog host contracts and extensions.
|
|
@@ -92,7 +94,7 @@ Selected cases are adapted from the ISO and standard-core suites of Scryer
|
|
|
92
94
|
Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
93
95
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
94
96
|
|
|
95
|
-
The corpus has
|
|
97
|
+
The corpus has 313 cases in `iso/` and 719 file-based conformance cases in
|
|
96
98
|
total. The generated `conformance-report.md` is the authoritative source for
|
|
97
99
|
current category totals. Together with regression, documentation-sync, API,
|
|
98
100
|
example, and book-example checks, `npm test` is the release gate.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
% Part 3 call//1, phrase//1, variable bodies, and semicontexts.
|
|
2
|
+
match(X, [X|S], S).
|
|
3
|
+
|
|
4
|
+
via_call(X) --> call(match(X)).
|
|
5
|
+
via_phrase --> phrase([x, y]).
|
|
6
|
+
look_ahead(X), [X] --> [X].
|
|
7
|
+
variable_body(Body) --> Body.
|
|
8
|
+
|
|
9
|
+
%% goal: phrase(via_call(a), [a])
|
|
10
|
+
%% goal: phrase(via_phrase, X)
|
|
11
|
+
%% goal: phrase(look_ahead(a), [a], R)
|
|
12
|
+
%% goal: phrase(variable_body([z]), X)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
% Part 3 alternatives, embedded goals, if-then-else, negation and cut.
|
|
2
|
+
token(X) --> [X], { atom(X) }.
|
|
3
|
+
choice --> [a] | [b].
|
|
4
|
+
guarded --> ([a] -> [b] ; [c]).
|
|
5
|
+
not_a --> \+ [a], [_].
|
|
6
|
+
committed --> ([a], ! ; [b]).
|
|
7
|
+
|
|
8
|
+
%% goal: phrase(token(a), [a])
|
|
9
|
+
%% goal: phrase(choice, X)
|
|
10
|
+
%% goal: phrase(guarded, X)
|
|
11
|
+
%% goal: phrase(not_a, [b])
|
|
12
|
+
%% goal: phrase(committed, X)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
%% goal: phrase([], not_a_terminal_sequence)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
%% goal: phrase([], [], not_a_terminal_sequence)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
%% goal: phrase(42, [])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
%% goal: phrase(Body, [])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(type_error(terminal_sequence), not_a_terminal_sequence)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(type_error(terminal_sequence), not_a_terminal_sequence)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(type_error(callable), 42)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(instantiation_error)
|
package/test/run-regression.mjs
CHANGED
|
@@ -595,7 +595,7 @@ function documentationSyncCases() {
|
|
|
595
595
|
'<a href="https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog">\n <img src="book-assets/title-page.svg" alt="Read The Art of EyeProlog"',
|
|
596
596
|
'README cover links to the book',
|
|
597
597
|
);
|
|
598
|
-
for (const filename of ['src/iso.js', 'src/standard-library.js', 'src/lib/eyeprolog.pl', 'src/lib/lists.pl', 'src/playground-worker.js']) {
|
|
598
|
+
for (const filename of ['src/iso.js', 'src/dcg.js', 'src/standard-library.js', 'src/lib/eyeprolog.pl', 'src/lib/lists.pl', 'src/playground-worker.js']) {
|
|
599
599
|
assertEqual(fs.existsSync(path.join(packageRoot, filename)), true, `${filename} exists`);
|
|
600
600
|
assertIncludes(book, filename, `book documents ${filename}`);
|
|
601
601
|
}
|
|
@@ -1030,7 +1030,9 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
1030
1030
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
1031
1031
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
1032
1032
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
1033
|
-
assertEqual(library.defs.size,
|
|
1033
|
+
assertEqual(library.defs.size, 129, 'EyeProlog registry contains only ISO host definitions');
|
|
1034
|
+
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
1035
|
+
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
1034
1036
|
assertEqual(registeredNativeEyePrologLibraryNames().length, 0, 'public native EyeProlog builtin count');
|
|
1035
1037
|
assertEqual(eyePrologPortableLibraryIndicators.length, 44, 'portable Prolog library count');
|
|
1036
1038
|
assertEqual(eyePrologNativeLibraryIndicators.length, 0, 'native host library count');
|
|
@@ -1111,6 +1113,26 @@ portable_check(A, B, C) :- lowercase('HELLO', A), replace('banana', 'na', 'NA',
|
|
|
1111
1113
|
'answer(blue, user_local).\nqualified(ok).\n', 'module execution');
|
|
1112
1114
|
},
|
|
1113
1115
|
},
|
|
1116
|
+
{
|
|
1117
|
+
name: 'Part 3 nonterminal indicators import through Part 2 modules',
|
|
1118
|
+
run: () => {
|
|
1119
|
+
const directory = path.join(tmp, `dcg-modules-${++tmpCounter}`);
|
|
1120
|
+
fs.mkdirSync(directory);
|
|
1121
|
+
fs.writeFileSync(path.join(directory, 'vocabulary.pl'), [
|
|
1122
|
+
':- module(vocabulary, [word//1]).',
|
|
1123
|
+
'word(hello) --> [hello].',
|
|
1124
|
+
'',
|
|
1125
|
+
].join('\n'));
|
|
1126
|
+
const source = [
|
|
1127
|
+
":- use_module('vocabulary.pl', [word//1]).",
|
|
1128
|
+
'answer(X) :- phrase(word(X), [hello]).',
|
|
1129
|
+
'',
|
|
1130
|
+
].join('\n');
|
|
1131
|
+
const program = Program.parseSources([{ text: source, filename: 'main.pl', baseDir: directory }]);
|
|
1132
|
+
assertEqual(program.findGroup('word', 3)?.module, 'vocabulary', 'word//1 imports expanded word/3');
|
|
1133
|
+
assertEqual(run(program, { goal: 'answer(X)' }).stdout, 'answer(hello).\n', 'imported grammar execution');
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1114
1136
|
{
|
|
1115
1137
|
name: 'EyeProlog library preserves relational and arithmetic behavior',
|
|
1116
1138
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1758,7 +1758,8 @@ truncate search; it does not prove that no further answer exists.
|
|
|
1758
1758
|
### Implementation boundary
|
|
1759
1759
|
|
|
1760
1760
|
The source layout mirrors the language boundary. `src/iso.js` contains the
|
|
1761
|
-
isolated ISO processor predicates and registry. `src/
|
|
1761
|
+
isolated ISO processor predicates and registry. `src/dcg.js` implements the
|
|
1762
|
+
shared ISO Part 3 grammar-rule and dynamic-body expansion. `src/lib/eyeprolog.pl` contains
|
|
1762
1763
|
collision-free portable extensions, while `src/lib/lists.pl` supplies common
|
|
1763
1764
|
list relations. Both are ordinary ISO/IEC 13211-2 modules, organized like
|
|
1764
1765
|
Scryer's `src/lib` and registered for `library(Name)` by
|
|
@@ -2594,40 +2595,37 @@ and exchanges left and right at every level. It also suggests a structural
|
|
|
2594
2595
|
induction. The empty tree is its own mirror; if both recursive calls are
|
|
2595
2596
|
correct, the constructed parent is correct.
|
|
2596
2597
|
|
|
2597
|
-
### A
|
|
2598
|
+
### A standard definite clause grammar
|
|
2598
2599
|
|
|
2599
|
-
|
|
2600
|
-
arguments
|
|
2600
|
+
ISO/IEC TS 13211-3 definite clause grammar rules describe a sequence while the
|
|
2601
|
+
processor supplies the pair of difference-list arguments used for execution:
|
|
2601
2602
|
|
|
2602
2603
|
```eyeprolog
|
|
2603
|
-
sentence
|
|
2604
|
-
noun_phrase(Input, AfterNoun),
|
|
2605
|
-
verb_phrase(AfterNoun, Rest).
|
|
2604
|
+
sentence --> noun_phrase, verb_phrase.
|
|
2606
2605
|
|
|
2607
|
-
noun_phrase
|
|
2608
|
-
noun_phrase
|
|
2606
|
+
noun_phrase --> [the], noun.
|
|
2607
|
+
noun_phrase --> [a], noun.
|
|
2609
2608
|
|
|
2610
|
-
noun
|
|
2611
|
-
noun
|
|
2609
|
+
noun --> [robot].
|
|
2610
|
+
noun --> [scientist].
|
|
2612
2611
|
|
|
2613
|
-
verb_phrase
|
|
2614
|
-
verb(Input, AfterVerb),
|
|
2615
|
-
noun_phrase(AfterVerb, Rest).
|
|
2612
|
+
verb_phrase --> verb, noun_phrase.
|
|
2616
2613
|
|
|
2617
|
-
verb
|
|
2618
|
-
verb
|
|
2614
|
+
verb --> [helps].
|
|
2615
|
+
verb --> [observes].
|
|
2619
2616
|
|
|
2620
|
-
complete_sentence(Words) :- sentence
|
|
2617
|
+
complete_sentence(Words) :- phrase(sentence, Words).
|
|
2621
2618
|
```
|
|
2622
2619
|
|
|
2623
2620
|
```sh
|
|
2624
2621
|
eyeprolog --goal 'complete_sentence([the, robot, helps, a, scientist])' program.pl
|
|
2625
2622
|
```
|
|
2626
2623
|
|
|
2627
|
-
The
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2624
|
+
The expansion of `sentence//0` is an ordinary `sentence/2` relation: its first
|
|
2625
|
+
extra argument is the sequence before parsing and its second is the suffix.
|
|
2626
|
+
Composition shares the suffix from `noun_phrase//0` with the input to
|
|
2627
|
+
`verb_phrase//0`. `phrase/2` requires complete consumption; `phrase/3` exposes
|
|
2628
|
+
the remaining sequence.
|
|
2631
2629
|
|
|
2632
2630
|
The same relation can be a bounded generator when vocabulary and output length
|
|
2633
2631
|
are constrained by surrounding relations. An unconstrained
|
|
@@ -5178,7 +5176,8 @@ The long catalogs are meant to be entered locally, not memorized linearly.
|
|
|
5178
5176
|
|
|
5179
5177
|
The standards baseline is ISO/IEC 13211-1:1995, as corrected by Technical
|
|
5180
5178
|
Corrigenda 1:2007, 2:2012, and 3:2017, together with the module facilities of
|
|
5181
|
-
ISO/IEC 13211-2
|
|
5179
|
+
ISO/IEC 13211-2 and definite clause grammars from ISO/IEC TS 13211-3:2025.
|
|
5180
|
+
EyeProlog implements the compatibility profile documented
|
|
5182
5181
|
here; it does not claim certification as a complete conforming processor.
|
|
5183
5182
|
|
|
5184
5183
|
Prolog source accepted by EyeProlog is UTF-8. `%` starts a line comment and
|
|
@@ -5244,6 +5243,7 @@ lowered to ordinary compound terms:
|
|
|
5244
5243
|
|
|
5245
5244
|
- prefix: `\+`, unary `+`, unary `-`, and `\`;
|
|
5246
5245
|
- control: `,`, `;`, and `->`;
|
|
5246
|
+
- grammar rules: `-->` and the Part 3 alternative `|`;
|
|
5247
5247
|
- unification and comparison: `=`, `\=`, `==`, `\==`, `@<`, `@=<`, `@>`,
|
|
5248
5248
|
`@>=`, `is`, `=:=`, `=\=`, `<`, `=<`, `>`, and `>=`;
|
|
5249
5249
|
- arithmetic: `+`, `-`, `*`, `/`, `//`, `div`, `mod`, `rem`, `/\`, `\/`,
|
|
@@ -5278,8 +5278,8 @@ negation.
|
|
|
5278
5278
|
|
|
5279
5279
|
EyeProlog supports cut, operator declarations, dynamic database updates, grouped
|
|
5280
5280
|
solutions, exceptions, flags, initialization and inclusion directives, and
|
|
5281
|
-
standard stream and term I/O. ISO Part 2 modules
|
|
5282
|
-
|
|
5281
|
+
standard stream and term I/O. ISO Part 2 modules and Part 3 definite clause
|
|
5282
|
+
grammars complement this Part 1 core.
|
|
5283
5283
|
|
|
5284
5284
|
### ISO Part 2 modules
|
|
5285
5285
|
|
|
@@ -5316,6 +5316,38 @@ an empty list when only qualified calls are wanted. `Module:Goal` selects a
|
|
|
5316
5316
|
module explicitly. Repeated module loads are idempotent, while conflicting
|
|
5317
5317
|
imports and requests for predicates that a module does not export are errors.
|
|
5318
5318
|
|
|
5319
|
+
### ISO Part 3 definite clause grammars
|
|
5320
|
+
|
|
5321
|
+
A grammar rule `Head --> Body.` is prepared as an ordinary predicate with two
|
|
5322
|
+
additional difference-list arguments. Parameterized nonterminals retain their
|
|
5323
|
+
written arguments, so `token(Type)//1` is implemented by `token/3`.
|
|
5324
|
+
Nonterminal indicators can be exported and imported through modules:
|
|
5325
|
+
|
|
5326
|
+
```text
|
|
5327
|
+
:- module(vocabulary, [word//1]).
|
|
5328
|
+
|
|
5329
|
+
word(noun) --> [robot] | [scientist].
|
|
5330
|
+
```
|
|
5331
|
+
|
|
5332
|
+
The required grammar constructs are terminal lists, `[]`, sequencing with
|
|
5333
|
+
comma, alternatives with `;` or `|`, if-then-else, embedded goals `{Goal}`,
|
|
5334
|
+
`call//1`, `phrase//1`, and `!//0`. Semicontexts provide look-ahead by restoring
|
|
5335
|
+
terminals to the remaining sequence:
|
|
5336
|
+
|
|
5337
|
+
```eyeprolog
|
|
5338
|
+
look_ahead(X), [X] --> [X].
|
|
5339
|
+
```
|
|
5340
|
+
|
|
5341
|
+
`phrase(+Body,?Sequence)` accepts or generates a complete sequence.
|
|
5342
|
+
`phrase(+Body,?Sequence,?Rest)` leaves `Rest` unconsumed and is steadfast in
|
|
5343
|
+
that argument. A variable body raises `instantiation_error`; a non-callable
|
|
5344
|
+
body raises `type_error(callable)`. EyeProlog performs the optional Part 3
|
|
5345
|
+
terminal-sequence checks and reports `type_error(terminal_sequence)`.
|
|
5346
|
+
|
|
5347
|
+
Part 3 leaves `\+//1` and standalone `->//2` implementation dependent.
|
|
5348
|
+
EyeProlog uses non-consuming negation (`\+ Body` tests from the current state)
|
|
5349
|
+
and threads the state produced by the condition into the then-grammar.
|
|
5350
|
+
|
|
5319
5351
|
### Directives and protected built-ins
|
|
5320
5352
|
|
|
5321
5353
|
`false/0` is the ISO always-failing built-in. It is protected as a static
|
|
@@ -5379,7 +5411,7 @@ profile. Where a predicate is defined by ISO/IEC 13211-1:1995, EyeProlog uses it
|
|
|
5379
5411
|
standard predicate indicator; the registry also includes a few later or common
|
|
5380
5412
|
compatibility predicates identified below. Arithmetic is expressed through
|
|
5381
5413
|
`is/2` rather than output arguments on arithmetic predicates. The registry
|
|
5382
|
-
contains
|
|
5414
|
+
contains 129 name/arity entries across 100 names.
|
|
5383
5415
|
|
|
5384
5416
|
| Role | Registered predicate indicators |
|
|
5385
5417
|
| --- | --- |
|
|
@@ -5389,6 +5421,7 @@ contains 127 name/arity entries across 99 names.
|
|
|
5389
5421
|
| Term order | `compare/3`, `@</2`, `@=</2`, `@>/2`, `@>=/2`, `sort/2`, `keysort/2` |
|
|
5390
5422
|
| Term inspection | `functor/3`, `arg/3`, `=../2`, `copy_term/2`, `term_variables/2` |
|
|
5391
5423
|
| Collection | `findall/3`, `bagof/3`, `setof/3` |
|
|
5424
|
+
| Grammar processing | `phrase/2`, `phrase/3` |
|
|
5392
5425
|
| Database and information | `clause/2`, `asserta/1`, `assertz/1`, `retract/1`, `retractall/1`, `abolish/1`, `current_predicate/1` |
|
|
5393
5426
|
| Operators, conversion, and flags | `op/3`, `current_op/3`, `char_conversion/2`, `current_char_conversion/2`, `current_prolog_flag/2`, `set_prolog_flag/2` |
|
|
5394
5427
|
| Atomic terms | `atom_length/2`, `atom_concat/3`, `sub_atom/5`, `atom_chars/2`, `atom_codes/2`, `char_code/2`, `number_chars/2`, `number_codes/2` |
|
|
@@ -5442,6 +5475,19 @@ if-then-else commitment described above. Cuts and committed conditions are
|
|
|
5442
5475
|
operational controls; use ordinary relations when all alternatives should
|
|
5443
5476
|
remain observable.
|
|
5444
5477
|
|
|
5478
|
+
### Definite clause grammar processing
|
|
5479
|
+
|
|
5480
|
+
| Predicate and principal call | Behavior |
|
|
5481
|
+
| --- | --- |
|
|
5482
|
+
| `phrase(+Body,?Sequence)` | Parses or generates `Sequence` with a Part 3 grammar body and requires complete consumption. |
|
|
5483
|
+
| `phrase(+Body,?Sequence,?Rest)` | Parses or generates a prefix described by `Body` and unifies `Rest` with the unconsumed terminal sequence. The final unification is delayed so the third argument is steadfast. |
|
|
5484
|
+
|
|
5485
|
+
Grammar rules are expanded during program preparation and therefore appear to
|
|
5486
|
+
the solver as ordinary predicates with two extra arguments. Dynamic grammar
|
|
5487
|
+
bodies passed to `phrase/2-3` use the same expansion rules. This includes
|
|
5488
|
+
module qualification and the caller module used by embedded or meta-called
|
|
5489
|
+
nonterminals.
|
|
5490
|
+
|
|
5445
5491
|
### Unification, type tests, and term order
|
|
5446
5492
|
|
|
5447
5493
|
| Predicate and principal call | Behavior |
|
|
@@ -5689,12 +5735,12 @@ so side effects occur in Prolog execution order.
|
|
|
5689
5735
|
|
|
5690
5736
|
### The EyeProlog library
|
|
5691
5737
|
|
|
5692
|
-
EyeProlog exposes **44 library predicate indicators** in addition to the
|
|
5738
|
+
EyeProlog exposes **44 library predicate indicators** in addition to the 129
|
|
5693
5739
|
indicators in its isolated ISO profile. **All 44 are ordinary Prolog clauses**
|
|
5694
5740
|
across `src/lib/eyeprolog.pl` and `src/lib/lists.pl`; none
|
|
5695
5741
|
is a native host predicate. The resulting
|
|
5696
|
-
normal EyeProlog language surface is therefore **
|
|
5697
|
-
indicators**. Internally, the runtime registry contains only the
|
|
5742
|
+
normal EyeProlog language surface is therefore **173 public predicate
|
|
5743
|
+
indicators**. Internally, the runtime registry contains only the 129 ISO
|
|
5698
5744
|
definitions; the EyeProlog relations are module source clauses.
|
|
5699
5745
|
|
|
5700
5746
|
The two Prolog files declare `eyeprolog` and `lists` with `module/2`. A program
|
|
@@ -6141,7 +6187,7 @@ Review questions:
|
|
|
6141
6187
|
</figure>
|
|
6142
6188
|
|
|
6143
6189
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
6144
|
-
top-level directory contains **
|
|
6190
|
+
top-level directory contains **189 self-contained runnable programs**. Every
|
|
6145
6191
|
source program has an exact answer file under
|
|
6146
6192
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **60 selected programs** have a checked
|
|
6147
6193
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic tables below link every top-level program and open the program
|
|
@@ -6193,6 +6239,7 @@ mode at a time.
|
|
|
6193
6239
|
| [Floating Point](https://github.com/eyereasoner/eyeprolog/blob/main/examples/floating-point.pl) | Floating-point arithmetic and comparisons. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/floating-point.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/floating-point.pl) |
|
|
6194
6240
|
| [Atomic conversion](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-atomic-conversion.pl) | Atom splitting, character atoms, Unicode codes, and numeric parsing. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-atomic-conversion.pl) |
|
|
6195
6241
|
| [Control and errors](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-control-and-errors.pl) | `call/1`, `once/1`, cut, if-then-else, `throw/1`, and `catch/3`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-control-and-errors.pl) |
|
|
6242
|
+
| [DCG command parser](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dcg-command-parser.pl) | A Part 3 grammar parses token lists into application terms, generates tokens, preserves a remainder, and rejects malformed input. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dcg-command-parser.pl) |
|
|
6196
6243
|
| [Dynamic database](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl) | Initialization and ordered updates to a declared dynamic procedure. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-dynamic-database.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/iso-dynamic-database.pl) |
|
|
6197
6244
|
| [Grouped solutions](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-grouped-solutions.pl) | `findall/3`, `bagof/3`, `setof/3`, existential qualification, and `clause/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-grouped-solutions.pl) |
|
|
6198
6245
|
| [Integer arithmetic](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-integer-arithmetic.pl) | Integer quotient/remainder choices plus bit operations. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-integer-arithmetic.pl) |
|
|
@@ -6511,7 +6558,7 @@ hand.
|
|
|
6511
6558
|
|
|
6512
6559
|
#### Running and extending the corpus
|
|
6513
6560
|
|
|
6514
|
-
Run all
|
|
6561
|
+
Run all 189 normal answer goldens and the 60 selected proof goldens with:
|
|
6515
6562
|
|
|
6516
6563
|
```sh
|
|
6517
6564
|
npm run test:examples
|
|
@@ -6559,9 +6606,10 @@ node test/run-conformance-report.mjs
|
|
|
6559
6606
|
```
|
|
6560
6607
|
|
|
6561
6608
|
The complete suite must pass before release. The file-based conformance corpus
|
|
6562
|
-
contains
|
|
6609
|
+
contains 719 cases, including 313 focused ISO
|
|
6563
6610
|
cases derived from the success, failure, mode, and error behavior in
|
|
6564
|
-
ISO/IEC 13211-1 clauses 7 and 8
|
|
6611
|
+
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
6612
|
+
Separate exact-output suites check 189 normal
|
|
6565
6613
|
examples and 60 proof examples; all extracted book programs are parsed and
|
|
6566
6614
|
their declared goals are executed. The seven-case
|
|
6567
6615
|
playground contract suite imports the production worker, sends real reasoning
|
|
@@ -6590,18 +6638,20 @@ Run the browser contract independently with:
|
|
|
6590
6638
|
npm run test:playground
|
|
6591
6639
|
```
|
|
6592
6640
|
|
|
6593
|
-
### Supported ISO Prolog
|
|
6641
|
+
### Supported ISO Prolog implementation
|
|
6594
6642
|
|
|
6595
|
-
EyeProlog executes a documented and tested
|
|
6596
|
-
ISO/IEC 13211-1:1995 and its three technical corrigenda
|
|
6597
|
-
|
|
6598
|
-
|
|
6643
|
+
EyeProlog executes a documented and tested ISO Prolog implementation based on
|
|
6644
|
+
ISO/IEC 13211-1:1995 and its three technical corrigenda, ISO/IEC 13211-2:2000,
|
|
6645
|
+
and ISO/IEC TS 13211-3:2025. The exact supported predicate indicators are
|
|
6646
|
+
listed in Chapter 39. The implementation includes control and
|
|
6599
6647
|
exceptions, term operations, arithmetic, grouped solutions, dynamic clauses,
|
|
6600
6648
|
operators, atomic-term processing, flags, character conversion, streams,
|
|
6601
6649
|
character/byte and term I/O, initialization, source inclusion, and
|
|
6602
6650
|
termination. Corrigendum 2 additions—including `subsumes_term/2`,
|
|
6603
6651
|
`acyclic_term/1`, `sort/2`, `keysort/2`, `term_variables/2`, `retractall/1`,
|
|
6604
|
-
and `call/2-8`—are part of that baseline rather than extensions.
|
|
6652
|
+
and `call/2-8`—are part of that baseline rather than extensions. Part 2 module
|
|
6653
|
+
identity and imports and Part 3 grammar expansion and `phrase/2-3` are likewise
|
|
6654
|
+
standard language facilities.
|
|
6605
6655
|
|
|
6606
6656
|
This breadth is not a formal certification of every processor requirement.
|
|
6607
6657
|
The executable examples are EyeProlog-profile programs using host-supplied goals,
|
|
@@ -6610,7 +6660,7 @@ The remaining qualifications are:
|
|
|
6610
6660
|
|
|
6611
6661
|
- zero-arity compound syntax such as `ready()` is represented by the atom
|
|
6612
6662
|
`ready`;
|
|
6613
|
-
- ISO Part 2 modules
|
|
6663
|
+
- ISO Part 2 modules and ISO/IEC TS 13211-3 definite clause grammars are supported;
|
|
6614
6664
|
- variables cannot occupy functor or predicate position;
|
|
6615
6665
|
- double-quoted text follows `double_quotes` exactly; the default `chars` value
|
|
6616
6666
|
matches Trealla and Scryer and may be changed to `codes` or `atom`;
|
|
@@ -41,7 +41,7 @@ for (const match of book.matchAll(/^(## \d+\. ([^\n]+)|# ([^\n]+)|```eyeprolog\n
|
|
|
41
41
|
.split('\n')
|
|
42
42
|
.map((line) => line.trim())
|
|
43
43
|
.find((line) => line && !line.startsWith('%') && !line.startsWith(':-'));
|
|
44
|
-
const predicate = firstClause?.match(/^([a-z][a-z0-9_]*)\s*(?:\(
|
|
44
|
+
const predicate = firstClause?.match(/^([a-z][a-z0-9_]*)\s*(?:\(|\.|-->)/)?.[1] ?? 'program';
|
|
45
45
|
const section = [...preceding.matchAll(/^### ([^\n]+)$/gm)].at(-1)?.[1] ?? '';
|
|
46
46
|
chapter.examples.push({ predicate, section, source });
|
|
47
47
|
}
|
package/why-eyeprolog.md
CHANGED
|
@@ -18,7 +18,8 @@ language gives programs recognizable semantics without inventing another rule
|
|
|
18
18
|
syntax.
|
|
19
19
|
|
|
20
20
|
EyeProlog implements the Part 1 core together with Technical Corrigenda 1, 2,
|
|
21
|
-
and 3
|
|
21
|
+
and 3, Part 2 modules, and the Part 3 definite clause grammar specification.
|
|
22
|
+
Its executable conformance matrix and tests document the supported
|
|
22
23
|
behavior. This is extensive implementation evidence, not certification by an
|
|
23
24
|
independent standards body.
|
|
24
25
|
|
|
@@ -31,6 +32,7 @@ keeps a narrow architecture:
|
|
|
31
32
|
- one solver with automatic tabling for eligible positive recursion;
|
|
32
33
|
- the ISO built-in registry;
|
|
33
34
|
- lean portable ISO Part 2 library modules;
|
|
35
|
+
- ISO Part 3 definite clause grammars and `phrase/2-3`;
|
|
34
36
|
- optional proof explanations; and
|
|
35
37
|
- the same implementation in Node.js and the browser.
|
|
36
38
|
|
|
@@ -79,5 +81,7 @@ visible enough to understand.
|
|
|
79
81
|
## References
|
|
80
82
|
|
|
81
83
|
- [ISO/IEC 13211-1:1995 — Prolog, Part 1: General core](https://www.iso.org/standard/21413.html)
|
|
84
|
+
- [ISO/IEC 13211-2:2000 — Prolog, Part 2: Modules](https://www.iso.org/standard/20775.html)
|
|
85
|
+
- [ISO/IEC TS 13211-3:2025 — Prolog, Part 3: Definite clause grammar rules](https://www.iso.org/standard/83635.html)
|
|
82
86
|
- [The Art of EyeProlog](the-art-of-eyeprolog.md)
|
|
83
87
|
- [EyeProlog README](README.md)
|