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.
Files changed (38) hide show
  1. package/README.md +12 -2
  2. package/conformance-report.md +2 -2
  3. package/examples/book/README.md +2 -1
  4. package/examples/book/chapter-22/04-sentence.pl +10 -14
  5. package/examples/book/chapter-38/02-look_ahead.pl +2 -0
  6. package/examples/dcg-command-parser.pl +26 -0
  7. package/examples/output/dcg-command-parser.pl +4 -0
  8. package/package.json +1 -1
  9. package/playground.html +1 -0
  10. package/src/dcg.js +241 -0
  11. package/src/iso.js +42 -0
  12. package/src/parser.js +23 -2
  13. package/src/program.js +22 -5
  14. package/src/solver.js +6 -2
  15. package/test/conformance/ISO-MATRIX.md +3 -1
  16. package/test/conformance/README.md +6 -4
  17. package/test/conformance/cases/iso/cut_disjunction_scope.pl +5 -0
  18. package/test/conformance/cases/iso/dcg_call_phrase_semicontext.pl +12 -0
  19. package/test/conformance/cases/iso/dcg_control_constructs.pl +12 -0
  20. package/test/conformance/cases/iso/dcg_module_nonterminal_indicator.pl +8 -0
  21. package/test/conformance/cases/iso/dcg_terminals_and_remainder.pl +8 -0
  22. package/test/conformance/errors/iso/dcg_phrase_bad_input.pl +1 -0
  23. package/test/conformance/errors/iso/dcg_phrase_bad_output.pl +1 -0
  24. package/test/conformance/errors/iso/dcg_phrase_noncallable.pl +1 -0
  25. package/test/conformance/errors/iso/dcg_phrase_variable.pl +1 -0
  26. package/test/conformance/expected/iso/cut_disjunction_scope.pl +2 -0
  27. package/test/conformance/expected/iso/dcg_call_phrase_semicontext.pl +4 -0
  28. package/test/conformance/expected/iso/dcg_control_constructs.pl +6 -0
  29. package/test/conformance/expected/iso/dcg_module_nonterminal_indicator.pl +2 -0
  30. package/test/conformance/expected/iso/dcg_terminals_and_remainder.pl +2 -0
  31. package/test/conformance/expected-errors/iso/dcg_phrase_bad_input.txt +1 -0
  32. package/test/conformance/expected-errors/iso/dcg_phrase_bad_output.txt +1 -0
  33. package/test/conformance/expected-errors/iso/dcg_phrase_noncallable.txt +1 -0
  34. package/test/conformance/expected-errors/iso/dcg_phrase_variable.txt +1 -0
  35. package/test/run-regression.mjs +24 -2
  36. package/the-art-of-eyeprolog.md +89 -39
  37. package/tools/extract-book-examples.mjs +1 -1
  38. 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
- ## Standard library modules
36
+ ## ISO modules and definite clause grammars
37
37
 
38
- EyeProlog adds 44 public library predicates to its 127-entry ISO core registry.
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
@@ -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 | 130 | 174 | 0 | 0 | 304 |
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** | **445** | **225** | **19** | **21** | **710** |
26
+ | **Total** | **450** | **229** | **19** | **21** | **719** |
@@ -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 grammar without special syntax
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 grammar without special syntax.
2
- sentence(Input, Rest) :-
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([the | Input], Rest) :- noun(Input, Rest).
7
- noun_phrase([a | Input], Rest) :- noun(Input, Rest).
4
+ noun_phrase --> [the], noun.
5
+ noun_phrase --> [a], noun.
8
6
 
9
- noun([robot | Rest], Rest).
10
- noun([scientist | Rest], Rest).
7
+ noun --> [robot].
8
+ noun --> [scientist].
11
9
 
12
- verb_phrase(Input, Rest) :-
13
- verb(Input, AfterVerb),
14
- noun_phrase(AfterVerb, Rest).
10
+ verb_phrase --> verb, noun_phrase.
15
11
 
16
- verb([helps | Rest], Rest).
17
- verb([observes | Rest], Rest).
12
+ verb --> [helps].
13
+ verb --> [observes].
18
14
 
19
- complete_sentence(Words) :- sentence(Words, []).
15
+ complete_sentence(Words) :- phrase(sentence, Words).
@@ -0,0 +1,2 @@
1
+ % From The Art of EyeProlog, Chapter 38 — ISO Part 3 definite clause grammars.
2
+ look_ahead(X), [X] --> [X].
@@ -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]).
@@ -0,0 +1,4 @@
1
+ dcg_example(parsed, set(light(kitchen), on)).
2
+ dcg_example(generated, [set, hall, light, to, off]).
3
+ dcg_example(remainder, [then, wait]).
4
+ dcg_example(rejected, invalid_command).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.0",
6
+ "version": "1.1.1",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/playground.html CHANGED
@@ -475,6 +475,7 @@
475
475
  "control-system",
476
476
  "critical-path-schedule",
477
477
  "cyclic-path",
478
+ "dcg-command-parser",
478
479
  "d3-group",
479
480
  "dairy-energy-balance",
480
481
  "data-negotiation",
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
- const head = this.parseTerm(3);
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 = context.module;
696
+ clause.module ??= context.module;
694
697
  if (!isDirectiveClause(clause)) {
695
- for (const goal of clause.body) annotateGoalModule(goal, context.module);
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 !== '/' || item.arity !== 2) return null;
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 === '/' && indicator.arity === 2
848
- ? predicateIndicator(indicator.args[0], indicator.args[1])
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/IEC 13211-1 conformance matrix
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 and all three
11
- corrigenda to representative executable cases.
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 standard families. This suite is still not an independent certification of
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 304 cases in `iso/` and 710 file-based conformance cases in
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,5 @@
1
+ % ISO cut discards choices in its own invocation but not its caller.
2
+ inner(X) :- (X = a, ! ; X = b).
3
+ outer(X) :- (inner(X) ; X = c).
4
+
5
+ %% goal: outer(X)
@@ -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,8 @@
1
+ % Part 2 module exports combined with a Part 3 nonterminal indicator.
2
+ :- module(dcg_vocab, [word//1, answer/1]).
3
+
4
+ word(X) --> [X].
5
+ answer(X) :- phrase(word(X), [hello]).
6
+
7
+ %% goal: dcg_vocab:answer(X)
8
+ %% goal: phrase(dcg_vocab:word(hello), [hello])
@@ -0,0 +1,8 @@
1
+ % ISO/IEC TS 13211-3 terminal sequences, concatenation, parsing and generation.
2
+ sentence --> article, noun, verb.
3
+ article --> [the].
4
+ noun --> [cat].
5
+ verb --> [sleeps].
6
+
7
+ %% goal: phrase(sentence, X)
8
+ %% goal: phrase(article, [the, rest], R)
@@ -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,2 @@
1
+ outer(a).
2
+ outer(c).
@@ -0,0 +1,4 @@
1
+ phrase(via_call(a), "a").
2
+ phrase(via_phrase, "xy").
3
+ phrase(look_ahead(a), "a", "a").
4
+ phrase(variable_body("z"), "z").
@@ -0,0 +1,6 @@
1
+ phrase(token(a), "a").
2
+ phrase(choice, "a").
3
+ phrase(choice, "b").
4
+ phrase(guarded, "ab").
5
+ phrase(not_a, "b").
6
+ phrase(committed, "a").
@@ -0,0 +1,2 @@
1
+ dcg_vocab:answer(hello).
2
+ phrase(dcg_vocab:word(hello), [hello]).
@@ -0,0 +1,2 @@
1
+ phrase(sentence, [the, cat, sleeps]).
2
+ phrase(article, [the, rest], [rest]).
@@ -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)
@@ -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, 127, 'EyeProlog registry contains only ISO host definitions');
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: () => {
@@ -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/lib/eyeprolog.pl` contains
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 grammar without special syntax
2598
+ ### A standard definite clause grammar
2598
2599
 
2599
- A recognizer can relate an input list to its unconsumed suffix. The pair of
2600
- arguments makes sequencing explicit:
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(Input, Rest) :-
2604
- noun_phrase(Input, AfterNoun),
2605
- verb_phrase(AfterNoun, Rest).
2604
+ sentence --> noun_phrase, verb_phrase.
2606
2605
 
2607
- noun_phrase([the | Input], Rest) :- noun(Input, Rest).
2608
- noun_phrase([a | Input], Rest) :- noun(Input, Rest).
2606
+ noun_phrase --> [the], noun.
2607
+ noun_phrase --> [a], noun.
2609
2608
 
2610
- noun([robot | Rest], Rest).
2611
- noun([scientist | Rest], Rest).
2609
+ noun --> [robot].
2610
+ noun --> [scientist].
2612
2611
 
2613
- verb_phrase(Input, Rest) :-
2614
- verb(Input, AfterVerb),
2615
- noun_phrase(AfterVerb, Rest).
2612
+ verb_phrase --> verb, noun_phrase.
2616
2613
 
2617
- verb([helps | Rest], Rest).
2618
- verb([observes | Rest], Rest).
2614
+ verb --> [helps].
2615
+ verb --> [observes].
2619
2616
 
2620
- complete_sentence(Words) :- sentence(Words, []).
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 first argument is the list before a phrase and the second is the suffix
2628
- after it. Composition is variable sharing: the suffix returned by
2629
- `noun_phrase/2` becomes the input of `verb_phrase/2`. This is the central idea
2630
- behind difference-list grammars, expressed without dedicated notation.
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. EyeProlog implements the compatibility profile documented
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 complement this Part 1 core;
5282
- DCG notation remains outside the profile.
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 127 name/arity entries across 99 names.
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 127
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 **171 public predicate
5697
- indicators**. Internally, the runtime registry contains only the 127 ISO
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 **188 self-contained runnable programs**. Every
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 188 normal answer goldens and the 60 selected proof goldens with:
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 710 cases, including 304 focused ISO
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. Separate exact-output suites check 188 normal
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 profile
6641
+ ### Supported ISO Prolog implementation
6594
6642
 
6595
- EyeProlog executes a documented and tested **ISO compatibility profile** based on
6596
- ISO/IEC 13211-1:1995 and its three technical corrigenda. The exact supported
6597
- predicate indicators—not a claim about the standard's complete processor
6598
- environment—are listed in Chapter 39. The profile includes control and
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 are supported; DCG notation remains outside the profile;
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*(?:\(|\.)/)?.[1] ?? 'program';
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. Its executable conformance matrix and tests document the supported
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)