eyeprolog 1.1.1 → 1.1.3

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