eyeprolog 1.1.6 → 1.1.8
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 +9 -0
- package/examples/book/README.md +4 -0
- package/examples/book/chapter-40/01-color.pl +10 -0
- package/index.d.ts +28 -2
- package/package.json +1 -1
- package/src/cli.js +16 -5
- package/src/dcg.js +51 -3
- package/src/index.js +1 -0
- package/src/iso.js +24 -9
- package/src/parser.js +61 -1
- package/src/program.js +13 -0
- package/src/quads.js +377 -0
- package/src/solver.js +6 -0
- package/src/write.js +1 -0
- package/test/conformance/THIRD_PARTY.md +2 -2
- package/test/conformance/expected-errors/iso/dcg_phrase_bad_input.txt +1 -1
- package/test/conformance/expected-errors/iso/dcg_phrase_bad_output.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_bad_input_precedence.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_bad_output_precedence.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_embedded_noncallable.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_improper_semicontext.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_improper_terminal_rule.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_invalid_semicontext.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_nested_noncallable.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_failure.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_success.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_input.txt +1 -1
- package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_output.txt +1 -1
- package/test/conformance/expected-errors/syntax/extra_double_period_rejected.txt +1 -1
- package/test/run-regression.mjs +164 -0
- package/the-art-of-eyeprolog.md +58 -3
package/README.md
CHANGED
|
@@ -45,6 +45,15 @@ Programs may declare their default queries with `%% goal:` comments.
|
|
|
45
45
|
Double-quoted text follows the ISO `double_quotes` flag and defaults to a
|
|
46
46
|
proper list of one-character atoms (`chars`), matching Trealla and Scryer.
|
|
47
47
|
|
|
48
|
+
Portable unit tests can be embedded as quads—a query followed by its expected
|
|
49
|
+
top-level answer—and run with `eyeprolog --quads program.pl`:
|
|
50
|
+
|
|
51
|
+
```prolog
|
|
52
|
+
member_test ?- member(X, [prolog, logic]).
|
|
53
|
+
X = prolog
|
|
54
|
+
; X = logic.
|
|
55
|
+
```
|
|
56
|
+
|
|
48
57
|
## ISO modules and definite clause grammars
|
|
49
58
|
|
|
50
59
|
EyeProlog implements ISO/IEC 13211-2 modules and the grammar rules and
|
package/examples/book/README.md
CHANGED
|
@@ -242,3 +242,7 @@ npm run generate
|
|
|
242
242
|
- [03-answer-3.pl](chapter-39/03-answer-3.pl)
|
|
243
243
|
- [04-cost.pl](chapter-39/04-cost.pl)
|
|
244
244
|
- [05-message.pl](chapter-39/05-message.pl)
|
|
245
|
+
|
|
246
|
+
## Chapter 40: Running EyeProlog: command line and corpus
|
|
247
|
+
|
|
248
|
+
- [01-color.pl](chapter-40/01-color.pl) — Embedded quad tests
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,29 @@ export interface EyePrologClause {
|
|
|
55
55
|
module?: string;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface EyePrologQuad {
|
|
59
|
+
kind: 'quad';
|
|
60
|
+
id: EyePrologTerm | null;
|
|
61
|
+
query: EyePrologTerm;
|
|
62
|
+
answers: EyePrologTerm[];
|
|
63
|
+
module?: string;
|
|
64
|
+
source: { filename: string; line: number };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface EyePrologQuadResult {
|
|
68
|
+
ok: boolean;
|
|
69
|
+
kind?: 'failed' | 'malformed' | 'bad_identifier' | 'unsupported';
|
|
70
|
+
expected?: EyePrologTerm;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface EyePrologQuadRunResult {
|
|
74
|
+
stdout: string;
|
|
75
|
+
total: number;
|
|
76
|
+
passed: number;
|
|
77
|
+
failed: number;
|
|
78
|
+
results: EyePrologQuadResult[];
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
export interface EyePrologPredicateGroup {
|
|
59
82
|
name: string;
|
|
60
83
|
arity: number;
|
|
@@ -94,6 +117,7 @@ export class Program {
|
|
|
94
117
|
groups: Map<string, EyePrologPredicateGroup>;
|
|
95
118
|
modules: Map<string, { name: string; exports: Map<string, unknown>; filename: string }>;
|
|
96
119
|
moduleImports: Map<string, Map<string, string>>;
|
|
120
|
+
quads: EyePrologQuad[];
|
|
97
121
|
doubleQuotes: 'chars' | 'codes' | 'atom';
|
|
98
122
|
negationDependencies: Array<{ from: string; to: string; negative: boolean }>;
|
|
99
123
|
negationStratificationErrors: Array<{ from: string; to: string }>;
|
|
@@ -186,8 +210,8 @@ export function numberTextFromDouble(value: number): string | null;
|
|
|
186
210
|
export function compareNumberText(left: string, right: string): number;
|
|
187
211
|
|
|
188
212
|
export function makeProgram(source: string, options?: EyePrologRunOptions): Program;
|
|
189
|
-
export function parseClauses(source: string, options?: EyePrologRunOptions): EyePrologClause
|
|
190
|
-
export function parseProgramText(source: string, options?: EyePrologRunOptions): EyePrologClause
|
|
213
|
+
export function parseClauses(source: string, options?: EyePrologRunOptions): Array<EyePrologClause | EyePrologQuad>;
|
|
214
|
+
export function parseProgramText(source: string, options?: EyePrologRunOptions): Array<EyePrologClause | EyePrologQuad>;
|
|
191
215
|
export function parseGoalText(source: string, options?: EyePrologRunOptions): EyePrologTerm;
|
|
192
216
|
export function createDefaultRegistry(): BuiltinRegistry;
|
|
193
217
|
export function createEyePrologRegistry(): BuiltinRegistry;
|
|
@@ -208,6 +232,7 @@ export class HaltSignal extends Error {
|
|
|
208
232
|
constructor(code?: number);
|
|
209
233
|
}
|
|
210
234
|
export function run(source: string | Program, options?: EyePrologRunOptions): EyePrologRunResult;
|
|
235
|
+
export function runQuads(source: string | Program, options?: EyePrologRunOptions & { initialize?: boolean }): EyePrologQuadRunResult;
|
|
211
236
|
export function whyProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): { ok: boolean; text: string };
|
|
212
237
|
export function whyNoProof(goal: EyePrologTerm): string;
|
|
213
238
|
export function explainProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): { ok: boolean; text: string };
|
|
@@ -269,6 +294,7 @@ declare const eyeprolog: {
|
|
|
269
294
|
eyePrologNativeLibraryIndicators: typeof eyePrologNativeLibraryIndicators;
|
|
270
295
|
eyePrologPortableLibraryIndicators: typeof eyePrologPortableLibraryIndicators;
|
|
271
296
|
run: typeof run;
|
|
297
|
+
runQuads: typeof runQuads;
|
|
272
298
|
whyProof: typeof whyProof;
|
|
273
299
|
whyNoProof: typeof whyNoProof;
|
|
274
300
|
explainProof: typeof explainProof;
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function main(argv) {
|
|
|
24
24
|
const options = {
|
|
25
25
|
files: [],
|
|
26
26
|
proof: false,
|
|
27
|
+
quads: false,
|
|
27
28
|
stats: false,
|
|
28
29
|
version: false,
|
|
29
30
|
warnings: false,
|
|
@@ -42,6 +43,8 @@ export async function main(argv) {
|
|
|
42
43
|
return;
|
|
43
44
|
} else if (!endOptions && (arg === '--proof' || arg === '-p')) {
|
|
44
45
|
options.proof = true;
|
|
46
|
+
} else if (!endOptions && (arg === '--quads' || arg === '-q')) {
|
|
47
|
+
options.quads = true;
|
|
45
48
|
} else if (!endOptions && (arg === '--stats' || arg === '-s')) {
|
|
46
49
|
options.stats = true;
|
|
47
50
|
} else if (!endOptions && (arg === '--version' || arg === '-v')) {
|
|
@@ -55,13 +58,14 @@ export async function main(argv) {
|
|
|
55
58
|
} else if (!endOptions && arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
|
|
56
59
|
const flags = arg.slice(1);
|
|
57
60
|
for (const flag of flags) {
|
|
58
|
-
if (!'
|
|
61
|
+
if (!'hpqsvw'.includes(flag)) throw new Error(`unknown option: ${arg}`);
|
|
59
62
|
}
|
|
60
63
|
if (flags.includes('h')) {
|
|
61
64
|
await usage(process.stdout);
|
|
62
65
|
return;
|
|
63
66
|
}
|
|
64
67
|
if (flags.includes('p')) options.proof = true;
|
|
68
|
+
if (flags.includes('q')) options.quads = true;
|
|
65
69
|
if (flags.includes('s')) options.stats = true;
|
|
66
70
|
if (flags.includes('v')) options.version = true;
|
|
67
71
|
if (flags.includes('w')) options.warnings = true;
|
|
@@ -102,7 +106,7 @@ export async function main(argv) {
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
if (options.goals.length === 0) {
|
|
109
|
+
if (options.goals.length === 0 && !options.quads) {
|
|
106
110
|
for (const source of sourceParts) options.goals.push(...goalsFromSource(source.text));
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -111,12 +115,17 @@ export async function main(argv) {
|
|
|
111
115
|
|
|
112
116
|
if (options.warnings) printWarnings(program);
|
|
113
117
|
|
|
114
|
-
await runDefault(engine, program, options);
|
|
118
|
+
if (!options.quads || options.goals.length > 0) await runDefault(engine, program, options);
|
|
119
|
+
if (options.quads) {
|
|
120
|
+
const result = engine.runQuads(program, { initialize: options.goals.length === 0 });
|
|
121
|
+
process.stdout.write(result.stdout);
|
|
122
|
+
if (result.failed > 0) process.exitCode = 1;
|
|
123
|
+
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
async function loadEngine() {
|
|
118
127
|
if (engineModule == null) {
|
|
119
|
-
const [term, parser, program, solver, iso, library, write] = await Promise.all([
|
|
128
|
+
const [term, parser, program, solver, iso, library, write, quads] = await Promise.all([
|
|
120
129
|
import('./term.js'),
|
|
121
130
|
import('./parser.js'),
|
|
122
131
|
import('./program.js'),
|
|
@@ -124,8 +133,9 @@ async function loadEngine() {
|
|
|
124
133
|
import('./iso.js'),
|
|
125
134
|
import('./standard-library.js'),
|
|
126
135
|
import('./write.js'),
|
|
136
|
+
import('./quads.js'),
|
|
127
137
|
]);
|
|
128
|
-
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write };
|
|
138
|
+
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads };
|
|
129
139
|
}
|
|
130
140
|
return engineModule;
|
|
131
141
|
}
|
|
@@ -207,6 +217,7 @@ Input:
|
|
|
207
217
|
Options:
|
|
208
218
|
-h, --help Show this help text and exit.
|
|
209
219
|
-p, --proof Enable proof explanations.
|
|
220
|
+
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
210
221
|
-s, --stats Print solver statistics to stderr after execution.
|
|
211
222
|
-v, --version Show the package version and exit.
|
|
212
223
|
-w, --warnings Print non-fatal portability warnings to stderr.
|
package/src/dcg.js
CHANGED
|
@@ -37,14 +37,14 @@ function terminalItems(term, env = new Env()) {
|
|
|
37
37
|
const seen = new Set();
|
|
38
38
|
let cursor = deref(term, env);
|
|
39
39
|
while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
|
|
40
|
-
if (seen.has(cursor)) throw new PrologError('type_error(
|
|
40
|
+
if (seen.has(cursor)) throw new PrologError('type_error(list)', original);
|
|
41
41
|
seen.add(cursor);
|
|
42
42
|
items.push(cursor.args[0]);
|
|
43
43
|
cursor = deref(cursor.args[1], env);
|
|
44
44
|
}
|
|
45
45
|
if (cursor.type === ATOM && cursor.name === '[]') return items;
|
|
46
46
|
if (cursor.type === VAR) throw new PrologError('instantiation_error');
|
|
47
|
-
throw new PrologError('type_error(
|
|
47
|
+
throw new PrologError('type_error(list)', original);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function terminalsGoal(terminals, input, output, env) {
|
|
@@ -155,8 +155,12 @@ export function expandDcgBody(body, input, output, options = {}) {
|
|
|
155
155
|
// definitions and documents these choices in its conformance profile.
|
|
156
156
|
if (body.type === COMPOUND && body.name === '\\+' && body.arity === 1) {
|
|
157
157
|
const ignored = freshDcgVariable('negated');
|
|
158
|
+
const negated = body.args[0];
|
|
159
|
+
const expandedNegated = negated.type === ATOM || negated.type === COMPOUND || negated.type === VAR
|
|
160
|
+
? expandDcgBody(negated, input, ignored, options)
|
|
161
|
+
: negated;
|
|
158
162
|
return conjunction(
|
|
159
|
-
compound('\\+', [
|
|
163
|
+
compound('\\+', [expandedNegated]),
|
|
160
164
|
equality(input, output),
|
|
161
165
|
);
|
|
162
166
|
}
|
|
@@ -175,6 +179,50 @@ export function expandDcgBody(body, input, output, options = {}) {
|
|
|
175
179
|
return appendStateArguments(body, input, output, module);
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// Embedded goals are validated before the translated grammar is executed.
|
|
183
|
+
// This keeps a non-callable goal visible even when an earlier terminal or
|
|
184
|
+
// branch would otherwise prevent the host goal from being reached.
|
|
185
|
+
export function validateDcgEmbeddedGoals(body, input, output) {
|
|
186
|
+
const invalidBody = invalidDcgControl(body);
|
|
187
|
+
if (invalidBody != null) throw new PrologError('type_error(callable)', invalidBody);
|
|
188
|
+
|
|
189
|
+
const visit = (term) => {
|
|
190
|
+
if (term.type !== COMPOUND) return;
|
|
191
|
+
if (term.name === '{}' && term.arity === 1) {
|
|
192
|
+
if (invalidControlGoal(term.args[0])) {
|
|
193
|
+
// Report the translated host-language conjunction, matching the
|
|
194
|
+
// convention used by Trealla and the ISO Part 3 quad corpus.
|
|
195
|
+
throw new PrologError('type_error(callable)', conjunction(term.args[0], equality(input, output)));
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if ([',', ';', '|', '->', '\\+'].includes(term.name)) {
|
|
200
|
+
for (const argument of term.args) visit(argument);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
visit(body);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function invalidDcgControl(term) {
|
|
207
|
+
if (term.type !== ATOM && term.type !== COMPOUND && term.type !== VAR) return term;
|
|
208
|
+
if (term.type !== COMPOUND || ![',', ';', '|', '->'].includes(term.name)) return null;
|
|
209
|
+
for (const argument of term.args) {
|
|
210
|
+
if (argument.type === COMPOUND && argument.name === '{}' && argument.arity === 1) continue;
|
|
211
|
+
const invalid = invalidDcgControl(argument);
|
|
212
|
+
if (invalid != null) return invalid;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function invalidControlGoal(goal) {
|
|
218
|
+
if (goal.type === VAR) return false;
|
|
219
|
+
if (goal.type !== ATOM && goal.type !== COMPOUND) return true;
|
|
220
|
+
if (goal.type === COMPOUND && [',', ';', '->'].includes(goal.name) && goal.arity === 2) {
|
|
221
|
+
return goal.args.some(invalidControlGoal);
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
178
226
|
function splitGrammarHead(head) {
|
|
179
227
|
let terminals = null;
|
|
180
228
|
if (head.type === COMPOUND && head.name === ',' && head.arity === 2) {
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
eyePrologPortableLibraryIndicators,
|
|
21
21
|
} from './standard-library.js';
|
|
22
22
|
export { StreamManager } from './io.js';
|
|
23
|
+
export { runQuads } from './quads.js';
|
|
23
24
|
|
|
24
25
|
import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround, termToString } from './term.js';
|
|
25
26
|
import { Program } from './program.js';
|
package/src/iso.js
CHANGED
|
@@ -7,7 +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
|
+
import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
|
|
11
11
|
|
|
12
12
|
let isoFresh = 0;
|
|
13
13
|
|
|
@@ -490,7 +490,7 @@ function* clauseBuiltin({ solver, goal, env }) {
|
|
|
490
490
|
if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
|
|
491
491
|
callableOrVariable(goal.args[1], env);
|
|
492
492
|
const indicator = compound('/', [atom(head.name), numberTerm(head.arity)]);
|
|
493
|
-
if (solver.registry.get(head.name, head.arity)) {
|
|
493
|
+
if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(head)) {
|
|
494
494
|
throw new PrologError('permission_error(access, private_procedure)', indicator);
|
|
495
495
|
}
|
|
496
496
|
const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
|
|
@@ -529,9 +529,13 @@ function procedureIndicator(head) {
|
|
|
529
529
|
return compound('/', [atom(head.name), numberTerm(head.arity)]);
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
function isGrammarRuleProcedure(head) {
|
|
533
|
+
return head.name === '-->' && head.arity === 2;
|
|
534
|
+
}
|
|
535
|
+
|
|
532
536
|
function assertModifiable(solver, head, module = 'user') {
|
|
533
537
|
const group = solver.program.findGroup(head.name, head.arity, head.module ?? module);
|
|
534
|
-
if (solver.registry.get(head.name, head.arity) || (group && !group.dynamic)) {
|
|
538
|
+
if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(head) || (group && !group.dynamic)) {
|
|
535
539
|
throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(head));
|
|
536
540
|
}
|
|
537
541
|
}
|
|
@@ -558,7 +562,7 @@ function* retractBuiltin({ solver, goal, env }) {
|
|
|
558
562
|
const parts = clauseParts(goal.args[0], env);
|
|
559
563
|
requireClauseHead(parts.head);
|
|
560
564
|
const group = solver.program.findGroup(parts.head.name, parts.head.arity, parts.head.module ?? goal.module ?? 'user');
|
|
561
|
-
if (solver.registry.get(parts.head.name, parts.head.arity) || (group && !group.dynamic)) {
|
|
565
|
+
if (solver.registry.get(parts.head.name, parts.head.arity) || isGrammarRuleProcedure(parts.head) || (group && !group.dynamic)) {
|
|
562
566
|
throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(parts.head));
|
|
563
567
|
}
|
|
564
568
|
if (!group) return;
|
|
@@ -581,7 +585,7 @@ function* retractAllBuiltin({ solver, goal, env }) {
|
|
|
581
585
|
const head = deref(goal.args[0], env);
|
|
582
586
|
requireClauseHead(head);
|
|
583
587
|
const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
|
|
584
|
-
if (solver.registry.get(head.name, head.arity) || (group && !group.dynamic)) {
|
|
588
|
+
if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(head) || (group && !group.dynamic)) {
|
|
585
589
|
throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(head));
|
|
586
590
|
}
|
|
587
591
|
if (group) {
|
|
@@ -615,7 +619,7 @@ function* abolishBuiltin({ solver, goal, env }) {
|
|
|
615
619
|
const target = predicateIndicatorParts(goal.args[0], env);
|
|
616
620
|
const module = goal.module ?? 'user';
|
|
617
621
|
const group = solver.program.findGroup(target.name, target.arity, module);
|
|
618
|
-
if (solver.registry.get(target.name, target.arity) || (group && !group.dynamic)) {
|
|
622
|
+
if (solver.registry.get(target.name, target.arity) || isGrammarRuleProcedure(target) || (group && !group.dynamic)) {
|
|
619
623
|
throw new PrologError('permission_error(modify, static_procedure)', target.indicator);
|
|
620
624
|
}
|
|
621
625
|
solver.program.abolishDynamicGroup(target.name, target.arity, module);
|
|
@@ -1519,8 +1523,19 @@ function callable(term, env) {
|
|
|
1519
1523
|
term = resolveCallable(term, env);
|
|
1520
1524
|
if (term.type === VAR) throw new PrologError('instantiation_error');
|
|
1521
1525
|
if (term.type !== ATOM && term.type !== COMPOUND) throw new PrologError('type_error(callable)', term);
|
|
1526
|
+
validateControlCallable(term, term);
|
|
1522
1527
|
return term;
|
|
1523
1528
|
}
|
|
1529
|
+
function validateControlCallable(term, culprit) {
|
|
1530
|
+
if (term.type !== COMPOUND || ![',', ';', '->'].includes(term.name) || term.arity !== 2) return;
|
|
1531
|
+
for (const argument of term.args) {
|
|
1532
|
+
if (argument.type === VAR) throw new PrologError('instantiation_error');
|
|
1533
|
+
if (argument.type !== ATOM && argument.type !== COMPOUND) {
|
|
1534
|
+
throw new PrologError('type_error(callable)', culprit);
|
|
1535
|
+
}
|
|
1536
|
+
validateControlCallable(argument, culprit);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1524
1539
|
function resolveCallable(term, env) {
|
|
1525
1540
|
const resolved = deref(term, env);
|
|
1526
1541
|
if (resolved.type !== COMPOUND) return resolved;
|
|
@@ -1555,14 +1570,14 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
1555
1570
|
if (grammarBody.type !== ATOM && grammarBody.type !== COMPOUND) {
|
|
1556
1571
|
throw new PrologError('type_error(callable)', grammarBody);
|
|
1557
1572
|
}
|
|
1558
|
-
|
|
1559
1573
|
const input = goal.args[1];
|
|
1560
1574
|
const requestedOutput = goal.arity === 2 ? emptyTerminalSequence() : goal.args[2];
|
|
1575
|
+
validateDcgEmbeddedGoals(grammarBody, input, requestedOutput);
|
|
1561
1576
|
if (!isListOrPartialList(input, env)) {
|
|
1562
|
-
throw new PrologError('type_error(
|
|
1577
|
+
throw new PrologError('type_error(list)', deref(input, env));
|
|
1563
1578
|
}
|
|
1564
1579
|
if (!isListOrPartialList(requestedOutput, env)) {
|
|
1565
|
-
throw new PrologError('type_error(
|
|
1580
|
+
throw new PrologError('type_error(list)', deref(requestedOutput, env));
|
|
1566
1581
|
}
|
|
1567
1582
|
|
|
1568
1583
|
// Delay the final output unification to keep phrase/3 steadfast in its
|
package/src/parser.js
CHANGED
|
@@ -33,7 +33,7 @@ function isPlainAtomStartCode(code) {
|
|
|
33
33
|
return code >= 97 && code <= 122;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const graphicAtomChars = '
|
|
36
|
+
const graphicAtomChars = '#$&*+-./<=>@^~\\:';
|
|
37
37
|
|
|
38
38
|
// ISO operator syntax is lowered to the same ordinary compound terms used by
|
|
39
39
|
// canonical notation. Commas remain separators except inside parentheses.
|
|
@@ -277,10 +277,30 @@ class Parser {
|
|
|
277
277
|
const line = this.line;
|
|
278
278
|
const ch = this.peek();
|
|
279
279
|
if (!ch) return { type: TOK.EOF, text: '', line };
|
|
280
|
+
if (this.source.startsWith('...', this.pos) && this.peek(3) !== '.') {
|
|
281
|
+
this.pos += 3;
|
|
282
|
+
return { type: TOK.ATOM, text: '...', line };
|
|
283
|
+
}
|
|
284
|
+
if (ch === '?' && this.peek(1) === '-') {
|
|
285
|
+
this.pos += 2;
|
|
286
|
+
return { type: TOK.ATOM, text: '?-', line };
|
|
287
|
+
}
|
|
288
|
+
if (ch === '.' && this.peek(1) &&
|
|
289
|
+
!isWhitespaceCode(this.peek(1).charCodeAt(0)) &&
|
|
290
|
+
this.peek(1) !== '%' && !(this.peek(1) === '/' && this.peek(2) === '*')) {
|
|
291
|
+
const start = this.pos;
|
|
292
|
+
this.take();
|
|
293
|
+
while (isGraphicAtomCode(this.peek().charCodeAt(0))) this.take();
|
|
294
|
+
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
295
|
+
}
|
|
280
296
|
if (ch === '!') {
|
|
281
297
|
this.take();
|
|
282
298
|
return { type: TOK.ATOM, text: '!', line };
|
|
283
299
|
}
|
|
300
|
+
if (ch === ';') {
|
|
301
|
+
this.take();
|
|
302
|
+
return { type: TOK.ATOM, text: ';', line };
|
|
303
|
+
}
|
|
284
304
|
|
|
285
305
|
const punct = {
|
|
286
306
|
'(': TOK.LPAREN, ')': TOK.RPAREN, '[': TOK.LBRACKET, ']': TOK.RBRACKET,
|
|
@@ -591,12 +611,47 @@ class Parser {
|
|
|
591
611
|
}
|
|
592
612
|
throw new Error(`parse line ${this.token.line}: bad term`);
|
|
593
613
|
}
|
|
614
|
+
sourceLineIsIndented(line) {
|
|
615
|
+
let start = 0;
|
|
616
|
+
for (let current = 1; current < line; current++) {
|
|
617
|
+
const newline = this.source.indexOf('\n', start);
|
|
618
|
+
if (newline < 0) return false;
|
|
619
|
+
start = newline + 1;
|
|
620
|
+
}
|
|
621
|
+
return this.source[start] === ' ' || this.source[start] === '\t';
|
|
622
|
+
}
|
|
623
|
+
parseQuad(id, line, accept) {
|
|
624
|
+
const query = this.parseTerm(0, true);
|
|
625
|
+
this.expect(TOK.DOT, '.');
|
|
626
|
+
this.advance();
|
|
627
|
+
|
|
628
|
+
const answers = [];
|
|
629
|
+
while (this.token.type !== TOK.EOF && this.sourceLineIsIndented(this.token.line)) {
|
|
630
|
+
answers.push(this.parseTerm(0, true));
|
|
631
|
+
this.expect(TOK.DOT, '.');
|
|
632
|
+
this.advance();
|
|
633
|
+
}
|
|
634
|
+
if (answers.length === 0) throw new Error(`parse line ${line}: quad requires an indented answer description`);
|
|
635
|
+
|
|
636
|
+
accept({
|
|
637
|
+
kind: 'quad',
|
|
638
|
+
id,
|
|
639
|
+
query,
|
|
640
|
+
answers,
|
|
641
|
+
source: { filename: this.filename, line },
|
|
642
|
+
});
|
|
643
|
+
}
|
|
594
644
|
parseProgram(emit = null) {
|
|
595
645
|
const clauses = emit ? null : [];
|
|
596
646
|
let clauseNumber = 0;
|
|
597
647
|
const accept = emit ?? ((clause) => clauses.push(clause));
|
|
598
648
|
while (this.token.type !== TOK.EOF) {
|
|
599
649
|
const line = this.token.line;
|
|
650
|
+
if (this.operatorTokenName() === '?-') {
|
|
651
|
+
this.advance();
|
|
652
|
+
this.parseQuad(null, line, accept);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
600
655
|
if (this.token.type === TOK.IF) {
|
|
601
656
|
this.advance();
|
|
602
657
|
const directive = this.parseTerm();
|
|
@@ -622,6 +677,11 @@ class Parser {
|
|
|
622
677
|
continue;
|
|
623
678
|
}
|
|
624
679
|
let head = this.parseTerm(3);
|
|
680
|
+
if (this.operatorTokenName() === '?-') {
|
|
681
|
+
this.advance();
|
|
682
|
+
this.parseQuad(head, line, accept);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
625
685
|
// ISO/IEC TS 13211-3 grammar rules use -->/2 at the same top-level
|
|
626
686
|
// priority as clauses. The left side may include an unparenthesized
|
|
627
687
|
// semicontext: NonTerminal, Terminals --> Body.
|
package/src/program.js
CHANGED
|
@@ -104,6 +104,7 @@ export class Program {
|
|
|
104
104
|
this.defineOperator(priority, specifier, name);
|
|
105
105
|
}
|
|
106
106
|
this.initializations = [];
|
|
107
|
+
this.quads = [];
|
|
107
108
|
this.prologFlagDirectives = [];
|
|
108
109
|
this.charConversionDirectives = [];
|
|
109
110
|
this.doubleQuotes = options.doubleQuotes ?? 'chars';
|
|
@@ -471,6 +472,12 @@ class ProgramBuilder {
|
|
|
471
472
|
let lastGroup = this.lastGroup;
|
|
472
473
|
|
|
473
474
|
for (const clause of clauses) {
|
|
475
|
+
if (clause?.kind === 'quad') {
|
|
476
|
+
const module = clause.module ?? 'user';
|
|
477
|
+
annotateGoalModule(clause.query, module);
|
|
478
|
+
program.quads.push({ ...clause, module });
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
474
481
|
clause.index = program.clauses.length;
|
|
475
482
|
program.clauses.push(clause);
|
|
476
483
|
|
|
@@ -664,6 +671,12 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
664
671
|
batch.length = 0;
|
|
665
672
|
};
|
|
666
673
|
const accept = (clause) => {
|
|
674
|
+
if (clause?.kind === 'quad') {
|
|
675
|
+
flush();
|
|
676
|
+
clause.module = context.module;
|
|
677
|
+
builder.addClauses([clause]);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
667
680
|
const grammarClause = expandDcgRuleClause(clause, context.module);
|
|
668
681
|
if (grammarClause) clause = grammarClause;
|
|
669
682
|
const moduleDeclaration = moduleDirective(clause);
|
package/src/quads.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Embedded quad tests: a query followed by one or more answer descriptions.
|
|
2
|
+
// The syntax follows the portable "queries using answer descriptions" format
|
|
3
|
+
// used by Trealla and the ISO Prolog working examples linked from issue #1.
|
|
4
|
+
import {
|
|
5
|
+
ATOM, COMPOUND, VAR, Env, atom, compound, copyResolved, deref,
|
|
6
|
+
flattenConjunction, properListItems, termIsGround,
|
|
7
|
+
unify, variable,
|
|
8
|
+
} from './term.js';
|
|
9
|
+
import { parseGoalText } from './parser.js';
|
|
10
|
+
import { Program } from './program.js';
|
|
11
|
+
import { Solver } from './solver.js';
|
|
12
|
+
import { getEyePrologRegistry } from './standard-library.js';
|
|
13
|
+
import { formatTermForWrite } from './write.js';
|
|
14
|
+
|
|
15
|
+
export function runQuads(source, options = {}) {
|
|
16
|
+
const program = source instanceof Program
|
|
17
|
+
? source
|
|
18
|
+
: Program.parse(source, { ...options, sourceMetadata: true });
|
|
19
|
+
const quads = program.quads ?? [];
|
|
20
|
+
if (quads.length === 0) {
|
|
21
|
+
return { stdout: 'quads: nothing to run.\n', total: 0, passed: 0, failed: 0, results: [] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.initialize !== false) {
|
|
25
|
+
const initializer = new Solver(program, {
|
|
26
|
+
...options,
|
|
27
|
+
registry: options.registry ?? getEyePrologRegistry(),
|
|
28
|
+
ioOptions: { write: () => {} },
|
|
29
|
+
});
|
|
30
|
+
initializer.runInitializations();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const results = [];
|
|
34
|
+
const lines = [];
|
|
35
|
+
for (const quad of quads) {
|
|
36
|
+
const result = checkQuad(program, quad, options);
|
|
37
|
+
results.push(result);
|
|
38
|
+
if (!result.ok) lines.push(formatFailure(program, quad, result));
|
|
39
|
+
}
|
|
40
|
+
const passed = results.filter((result) => result.ok).length;
|
|
41
|
+
const failed = results.length - passed;
|
|
42
|
+
lines.push(`quads: ${results.length} run, ${passed} passed, ${failed} failed.\n`);
|
|
43
|
+
return { stdout: lines.join(''), total: results.length, passed, failed, results };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkQuad(program, quad, options) {
|
|
47
|
+
if (quad.id != null && !termIsGround(quad.id, new Env())) {
|
|
48
|
+
return { ok: false, kind: 'bad_identifier', expected: quad.id };
|
|
49
|
+
}
|
|
50
|
+
for (const description of quad.answers) {
|
|
51
|
+
const checked = checkDescription(program, quad, description, options);
|
|
52
|
+
if (!checked.ok) return checked;
|
|
53
|
+
}
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkDescription(program, quad, description, options) {
|
|
58
|
+
const alternatives = splitOperator(description, '|');
|
|
59
|
+
for (const alternative of alternatives) {
|
|
60
|
+
const malformed = malformedAlternative(quad.query, alternative);
|
|
61
|
+
if (malformed != null) return { ok: false, kind: 'malformed', expected: malformed };
|
|
62
|
+
}
|
|
63
|
+
let unsupported = null;
|
|
64
|
+
for (const alternative of alternatives) {
|
|
65
|
+
const checked = checkAlternative(program, quad, alternative, options);
|
|
66
|
+
if (checked.ok) return checked;
|
|
67
|
+
if (checked.kind === 'unsupported') unsupported ??= checked;
|
|
68
|
+
}
|
|
69
|
+
return unsupported ?? { ok: false, kind: 'failed', expected: description };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkAlternative(program, quad, alternative, options) {
|
|
73
|
+
const leaves = splitOperator(alternative, ';').map(describeLeaf);
|
|
74
|
+
if (leaves.some((leaf) => leaf.sto)) return { ok: true };
|
|
75
|
+
const unsupported = leaves.find((leaf) => leaf.unsupported != null)?.unsupported;
|
|
76
|
+
if (unsupported != null) {
|
|
77
|
+
return { ok: false, kind: 'unsupported', expected: unsupported };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const inputSpecs = [...new Set(leaves.filter((leaf) => leaf.input != null).map((leaf) => leaf.input))];
|
|
81
|
+
if (inputSpecs.length > 1) return { ok: false, kind: 'malformed', expected: alternative };
|
|
82
|
+
const input = inputSpecs[0] ?? '';
|
|
83
|
+
if (inputSpecs.length > 0 && leaves.length !== 1) return { ok: false };
|
|
84
|
+
const moreAt = leaves.findIndex((leaf) => leaf.more);
|
|
85
|
+
const describedCount = moreAt < 0 ? leaves.length : moreAt + (leaves[moreAt].hasExpectation ? 1 : 0);
|
|
86
|
+
const maxSolutions = inputSpecs.length > 0
|
|
87
|
+
? 1
|
|
88
|
+
: moreAt < 0 ? describedCount + 1 : Math.max(describedCount, 1);
|
|
89
|
+
const actual = executeQuery(program, quad.query, input, maxSolutions, options);
|
|
90
|
+
|
|
91
|
+
if (inputSpecs.length > 0) {
|
|
92
|
+
const leaf = leaves[0];
|
|
93
|
+
const matches = actual.inputPosition === input.length && matchLeaf(quad.query, leaf, actual, 0);
|
|
94
|
+
return { ok: leaf.unexpected ? !matches : matches };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let position = 0;
|
|
98
|
+
for (const leaf of leaves) {
|
|
99
|
+
if (leaf.more && !leaf.hasExpectation) return { ok: true };
|
|
100
|
+
const matches = matchLeaf(quad.query, leaf, actual, position);
|
|
101
|
+
if (leaf.unexpected ? matches : !matches) return { ok: false };
|
|
102
|
+
if (leaf.more) return { ok: true };
|
|
103
|
+
if (!leaf.unexpected && (leaf.false || leaf.error != null)) {
|
|
104
|
+
return { ok: position === leaves.length - 1 };
|
|
105
|
+
}
|
|
106
|
+
position++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ended = position >= actual.solutions.length && actual.error == null;
|
|
110
|
+
return { ok: ended };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function malformedAlternative(query, alternative) {
|
|
114
|
+
const queryNames = new Set(namedVariables(query).map((variable) => variable.name));
|
|
115
|
+
for (const leaf of splitOperator(alternative, ';').map(describeLeaf)) {
|
|
116
|
+
if (leaf.malformed != null) return leaf.malformed;
|
|
117
|
+
const names = new Set();
|
|
118
|
+
for (const binding of leaf.bindings) {
|
|
119
|
+
const name = binding.args[0].name;
|
|
120
|
+
if (!queryNames.has(name) || names.has(name)) return binding;
|
|
121
|
+
names.add(name);
|
|
122
|
+
}
|
|
123
|
+
if (!leaf.sto) {
|
|
124
|
+
for (const binding of leaf.bindings) {
|
|
125
|
+
if (namedVariables(binding.args[1]).some((variable) => names.has(variable.name))) return binding;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function describeLeaf(term) {
|
|
133
|
+
const leaf = {
|
|
134
|
+
bindings: [],
|
|
135
|
+
unexpected: false,
|
|
136
|
+
more: false,
|
|
137
|
+
sto: false,
|
|
138
|
+
false: false,
|
|
139
|
+
truth: false,
|
|
140
|
+
error: null,
|
|
141
|
+
input: null,
|
|
142
|
+
output: null,
|
|
143
|
+
unsupported: null,
|
|
144
|
+
malformed: null,
|
|
145
|
+
hasExpectation: false,
|
|
146
|
+
};
|
|
147
|
+
for (const item of flattenConjunction(term)) {
|
|
148
|
+
if (item.type === ATOM) {
|
|
149
|
+
if (item.name === 'unexpected' || item.name === 'inattendue') leaf.unexpected = true;
|
|
150
|
+
else if (item.name === '...' || item.name === 'ad_infinitum') leaf.more = true;
|
|
151
|
+
else if (item.name === 'sto') leaf.sto = true;
|
|
152
|
+
else if (item.name === 'loops' || item.name === 'waits' || item.name === 'other_answer_sequence') {
|
|
153
|
+
leaf.unsupported ??= item;
|
|
154
|
+
} else if (item.name === 'false') leaf.false = true;
|
|
155
|
+
else if (item.name === 'true') leaf.truth = true;
|
|
156
|
+
else if (isErrorDescription(item)) leaf.error = item;
|
|
157
|
+
else leaf.malformed ??= item;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (item.type === COMPOUND && item.name === '=' && item.arity === 2) {
|
|
161
|
+
if (item.args[0].type !== VAR) leaf.malformed ??= item;
|
|
162
|
+
else leaf.bindings.push(item);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (item.type === COMPOUND && item.name === 'inputs' && item.arity === 1) {
|
|
166
|
+
const text = characterText(item.args[0]);
|
|
167
|
+
if (text == null || leaf.input != null) leaf.malformed ??= item;
|
|
168
|
+
else leaf.input = text;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (item.type === COMPOUND && item.name === 'outputs' && item.arity === 1) {
|
|
172
|
+
const text = characterText(item.args[0]);
|
|
173
|
+
if (text == null || leaf.output != null) leaf.malformed ??= item;
|
|
174
|
+
else leaf.output = text;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (item.type === COMPOUND && item.name === 'peeks' && item.arity === 1) {
|
|
178
|
+
leaf.unsupported ??= item;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (isErrorDescription(item)) leaf.error = item;
|
|
182
|
+
else leaf.malformed ??= item;
|
|
183
|
+
}
|
|
184
|
+
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.error != null || leaf.output != null;
|
|
185
|
+
if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
|
|
186
|
+
if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
|
|
187
|
+
return leaf;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function executeQuery(program, query, input, maxSolutions, options) {
|
|
191
|
+
let pendingOutput = '';
|
|
192
|
+
const solver = new Solver(program, {
|
|
193
|
+
...options,
|
|
194
|
+
registry: options.registry ?? getEyePrologRegistry(),
|
|
195
|
+
// The solver's counter also observes completed nested searches (for
|
|
196
|
+
// example each arm of a DCG disjunction). Bound the public iterator here
|
|
197
|
+
// instead of letting those internal completions consume the quad's answer
|
|
198
|
+
// allowance.
|
|
199
|
+
solutionLimit: Math.max(maxSolutions, options.solutionLimit ?? 10000000),
|
|
200
|
+
ioOptions: {
|
|
201
|
+
input,
|
|
202
|
+
write: (text) => { pendingOutput += String(text); },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
// Undefined predicates are test failures rather than silent negative
|
|
206
|
+
// answers unless the source explicitly selected another unknown policy.
|
|
207
|
+
if (!(program.prologFlagDirectives ?? []).some(([flag]) => flag.type === ATOM && flag.name === 'unknown')) {
|
|
208
|
+
solver.prologFlags.get('unknown').value = atom('error');
|
|
209
|
+
}
|
|
210
|
+
const solutions = [];
|
|
211
|
+
let error = null;
|
|
212
|
+
let tailOutput = '';
|
|
213
|
+
try {
|
|
214
|
+
const iterator = solver.solve([query], new Env(), 0);
|
|
215
|
+
while (solutions.length < maxSolutions) {
|
|
216
|
+
pendingOutput = '';
|
|
217
|
+
const result = iterator.next();
|
|
218
|
+
if (result.done) {
|
|
219
|
+
tailOutput += pendingOutput;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
solutions.push({ env: result.value, output: pendingOutput });
|
|
223
|
+
}
|
|
224
|
+
} catch (caught) {
|
|
225
|
+
error = { term: errorTerm(caught), output: pendingOutput };
|
|
226
|
+
}
|
|
227
|
+
const inputPosition = solver.io.resolve('user_input')?.position ?? 0;
|
|
228
|
+
return { solutions, error, tailOutput, inputPosition };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function matchLeaf(query, leaf, actual, position) {
|
|
232
|
+
if (leaf.false) {
|
|
233
|
+
return position >= actual.solutions.length && actual.error == null && outputMatches(leaf.output, actual.tailOutput);
|
|
234
|
+
}
|
|
235
|
+
if (leaf.error != null) {
|
|
236
|
+
return position === actual.solutions.length && actual.error != null &&
|
|
237
|
+
errorMatches(leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
|
|
238
|
+
}
|
|
239
|
+
const solution = actual.solutions[position];
|
|
240
|
+
if (!solution || !outputMatches(leaf.output, solution.output)) return false;
|
|
241
|
+
return substitutionMatches(query, leaf.bindings, solution.env);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function substitutionMatches(query, bindings, actualEnv) {
|
|
245
|
+
const queryVariables = namedVariables(query);
|
|
246
|
+
const queryNames = new Set(queryVariables.map((variable) => variable.name));
|
|
247
|
+
const expectedEnv = new Env();
|
|
248
|
+
const rebound = new Set();
|
|
249
|
+
for (const binding of bindings) {
|
|
250
|
+
const variable = binding.args[0];
|
|
251
|
+
if (!queryNames.has(variable.name) || rebound.has(variable.name)) return false;
|
|
252
|
+
rebound.add(variable.name);
|
|
253
|
+
if (!unify(variable, binding.args[1], expectedEnv)) return false;
|
|
254
|
+
}
|
|
255
|
+
const expected = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, expectedEnv)));
|
|
256
|
+
const actual = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, actualEnv)));
|
|
257
|
+
return patternVariant(expected, new Env(), actual, new Env());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function namedVariables(term) {
|
|
261
|
+
const found = [];
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
const stack = [term];
|
|
264
|
+
while (stack.length) {
|
|
265
|
+
const current = stack.pop();
|
|
266
|
+
if (current.type === VAR) {
|
|
267
|
+
if (!current.name.startsWith('__anon') && !seen.has(current.name)) {
|
|
268
|
+
seen.add(current.name);
|
|
269
|
+
found.push(current);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
for (let index = current.args.length - 1; index >= 0; index--) stack.push(current.args[index]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function patternVariant(pattern, patternEnv, actual, actualEnv, pairs = new Map(), reverse = new Map()) {
|
|
279
|
+
pattern = deref(pattern, patternEnv);
|
|
280
|
+
actual = deref(actual, actualEnv);
|
|
281
|
+
if (pattern.type === ATOM && pattern.name === '...') return true;
|
|
282
|
+
if (pattern.type === VAR || actual.type === VAR) {
|
|
283
|
+
if (pattern.type !== VAR || actual.type !== VAR) return false;
|
|
284
|
+
const paired = pairs.get(pattern.name);
|
|
285
|
+
const reversed = reverse.get(actual.name);
|
|
286
|
+
if (paired != null || reversed != null) return paired === actual.name && reversed === pattern.name;
|
|
287
|
+
pairs.set(pattern.name, actual.name);
|
|
288
|
+
reverse.set(actual.name, pattern.name);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
if (pattern.type !== actual.type || pattern.name !== actual.name || pattern.arity !== actual.arity) return false;
|
|
292
|
+
for (let index = 0; index < pattern.arity; index++) {
|
|
293
|
+
if (!patternVariant(pattern.args[index], patternEnv, actual.args[index], actualEnv, pairs, reverse)) return false;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function errorTerm(error) {
|
|
299
|
+
if (error?.name === 'ThrownTerm' && error.term) return compound('$quad_thrown', [error.term]);
|
|
300
|
+
if (error?.name === 'PrologError') {
|
|
301
|
+
let formal;
|
|
302
|
+
try {
|
|
303
|
+
formal = parseGoalText(error.formal);
|
|
304
|
+
} catch (_) {
|
|
305
|
+
formal = atom(error.formal ?? 'system_error');
|
|
306
|
+
}
|
|
307
|
+
if (error.culprit != null) formal = formal.type === COMPOUND
|
|
308
|
+
? compound(formal.name, [...formal.args, error.culprit])
|
|
309
|
+
: compound(formal.name, [error.culprit]);
|
|
310
|
+
return compound('error', [formal, variable('$quad_context')]);
|
|
311
|
+
}
|
|
312
|
+
return compound('error', [atom('system_error'), variable('$quad_context')]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function errorMatches(expected, actual) {
|
|
316
|
+
if (expected.type === COMPOUND && expected.name === 'throw' && expected.arity === 1 &&
|
|
317
|
+
actual.type === COMPOUND && actual.name === '$quad_thrown' && actual.arity === 1) {
|
|
318
|
+
return patternVariant(expected.args[0], new Env(), actual.args[0], new Env());
|
|
319
|
+
}
|
|
320
|
+
if (actual.type !== COMPOUND || actual.name !== 'error' || actual.arity !== 2) return false;
|
|
321
|
+
if (expected.type === COMPOUND && expected.name === 'error' && expected.arity === 2) {
|
|
322
|
+
return patternVariant(expected, new Env(), actual, new Env());
|
|
323
|
+
}
|
|
324
|
+
return patternVariant(expected, new Env(), actual.args[0], new Env());
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isErrorDescription(term) {
|
|
328
|
+
if (term.type === ATOM) return ['instantiation_error', 'system_error'].includes(term.name);
|
|
329
|
+
return term.type === COMPOUND && [
|
|
330
|
+
'error', 'throw', 'type_error', 'domain_error', 'existence_error',
|
|
331
|
+
'permission_error', 'evaluation_error', 'representation_error',
|
|
332
|
+
'resource_error', 'syntax_error', 'uninstantiation_error',
|
|
333
|
+
].includes(term.name);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function characterText(term) {
|
|
337
|
+
const items = properListItems(term, new Env());
|
|
338
|
+
if (items == null) return null;
|
|
339
|
+
let text = '';
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
if (item.type === ATOM && Array.from(item.name).length === 1) text += item.name;
|
|
342
|
+
else if (item.type === 'number' && /^\d+$/.test(item.name)) text += String.fromCodePoint(Number(item.name));
|
|
343
|
+
else return null;
|
|
344
|
+
}
|
|
345
|
+
return text;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function outputMatches(expected, actual) {
|
|
349
|
+
return expected == null || expected === actual;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function splitOperator(term, name) {
|
|
353
|
+
if (term.type === COMPOUND && term.name === name && term.arity === 2) {
|
|
354
|
+
return [term.args[0], ...splitOperator(term.args[1], name)];
|
|
355
|
+
}
|
|
356
|
+
return [term];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function formatFailure(program, quad, result) {
|
|
360
|
+
const source = quad.source ?? { filename: '<input>', line: 1 };
|
|
361
|
+
const label = quad.id == null ? '' : `${formatQuadTerm(program, quad.id)}, `;
|
|
362
|
+
const reason = result.kind === 'malformed' ? 'MALFORMED'
|
|
363
|
+
: result.kind === 'bad_identifier' ? 'BAD_ID'
|
|
364
|
+
: result.kind === 'unsupported' ? 'UNSUPPORTED'
|
|
365
|
+
: 'FAILED';
|
|
366
|
+
const expected = result.expected ?? quad.answers[0];
|
|
367
|
+
return `quads: ${reason} ${label}${source.filename}:${source.line}\n` +
|
|
368
|
+
` ?- ${formatQuadTerm(program, quad.query)}.\n` +
|
|
369
|
+
` expected: ${formatQuadTerm(program, expected)}.\n`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function formatQuadTerm(program, term) {
|
|
373
|
+
return formatTermForWrite(term, new Env(), {
|
|
374
|
+
quoted: true,
|
|
375
|
+
operators: [...program.operators.values()],
|
|
376
|
+
});
|
|
377
|
+
}
|
package/src/solver.js
CHANGED
|
@@ -281,6 +281,12 @@ export class Solver {
|
|
|
281
281
|
this.stats.solve_one_goal_calls++;
|
|
282
282
|
const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
283
283
|
if (!group) {
|
|
284
|
+
if (goal.name === '-->' && goal.arity === 2) {
|
|
285
|
+
throw new PrologError(
|
|
286
|
+
'existence_error(procedure)',
|
|
287
|
+
compound('/', [compound('-->', []), numberTerm(2)]),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
284
290
|
if (this.prologFlags.get('unknown')?.value?.name === 'error') {
|
|
285
291
|
throw new PrologError(
|
|
286
292
|
'existence_error(procedure)',
|
package/src/write.js
CHANGED
|
@@ -9,6 +9,7 @@ const graphicAtomCharacters = new Set('!#$&*+-/<=>@^~\\'.split(''));
|
|
|
9
9
|
function atomNeedsQuotes(name) {
|
|
10
10
|
if (!name) return true;
|
|
11
11
|
if (name === '[]' || name === '{}') return false;
|
|
12
|
+
if (name === '...') return false;
|
|
12
13
|
if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
|
|
13
14
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
|
|
14
15
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
|
@@ -13,8 +13,8 @@ Part 3 grammar cases are additionally adapted from Logtalk's
|
|
|
13
13
|
`tests/logtalk/methods/phrase_2_3/tests.lgt` and
|
|
14
14
|
`tests/logtalk/dcgs/tests.lgt` suites. Object and unit-test scaffolding was
|
|
15
15
|
removed, translator-only assertions were converted to executable grammar
|
|
16
|
-
behavior where possible, and expected list errors
|
|
17
|
-
`
|
|
16
|
+
behavior where possible, and expected list errors use the portable ISO
|
|
17
|
+
`type_error(list)` term.
|
|
18
18
|
|
|
19
19
|
Copyright 1998-2026 Paulo Moura <pmoura@logtalk.org>
|
|
20
20
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), not_a_terminal_sequence)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), not_a_terminal_sequence)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), not_a_terminal_sequence)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), not_a_terminal_sequence)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(callable), 1)
|
|
1
|
+
error(type_error(callable), (1, =([], [])))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), [a | b])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), [a | b])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), b)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(callable), 1)
|
|
1
|
+
error(type_error(callable), (1, =([__anon1], [])))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(callable), 1)
|
|
1
|
+
error(type_error(callable), (1, =("y", [])))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(callable), 1)
|
|
1
|
+
error(type_error(callable), (1, =([__anon1], [])))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), [a | b])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
error(type_error(
|
|
1
|
+
error(type_error(list), [a | b])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: expected ., got ..
|
package/test/run-regression.mjs
CHANGED
|
@@ -183,6 +183,169 @@ why(
|
|
|
183
183
|
assertNotIncludes(result.stdout, 'no_proof', 'stdout');
|
|
184
184
|
},
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'parser records embedded quads without indexing them as clauses',
|
|
188
|
+
run: () => {
|
|
189
|
+
const source = `p(1).\n\nnamed ?- p(X).\n X = 1.\n\nq(2).\n`;
|
|
190
|
+
const program = Program.parseSources([{ text: source, filename: 'embedded-quads.pl' }]);
|
|
191
|
+
assertEqual(program.clauses.length, 2, 'ordinary clause count');
|
|
192
|
+
assertEqual(program.quads.length, 1, 'quad count');
|
|
193
|
+
assertEqual(program.quads[0].id.name, 'named', 'quad label');
|
|
194
|
+
assertEqual(program.quads[0].source.filename, 'embedded-quads.pl', 'quad filename');
|
|
195
|
+
assertEqual(program.quads[0].source.line, 3, 'quad line');
|
|
196
|
+
assertEqual(Boolean(program.findGroup('p', 1)), true, 'preceding clause indexed');
|
|
197
|
+
assertEqual(Boolean(program.findGroup('q', 1)), true, 'following clause indexed');
|
|
198
|
+
assertEqual(Boolean(program.findGroup('?-', 2)), false, 'quad is inert');
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'parser separates compact ISO solo tokens and atom dots',
|
|
203
|
+
run: () => {
|
|
204
|
+
const program = Program.parse(
|
|
205
|
+
`compact ?- call((!;\\+1)).\n true.\n\n` +
|
|
206
|
+
`dot ?- functor([_],.,2).\n true.\n`,
|
|
207
|
+
);
|
|
208
|
+
assertEqual(program.quads.length, 2, 'quad count');
|
|
209
|
+
assertEqual(program.quads[0].query.args[0].name, ';', 'disjunction');
|
|
210
|
+
assertEqual(program.quads[0].query.args[0].args[1].name, '\\+', 'negation');
|
|
211
|
+
assertEqual(program.quads[1].query.args[1].name, '.', 'dot atom');
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'runQuads checks portable answer descriptions',
|
|
216
|
+
run: () => {
|
|
217
|
+
const source = `p(1).\np(2).\np(3).\n\n` +
|
|
218
|
+
`ordered ?- p(X).\n X = 1 ; X = 2 ; X = 3.\n\n` +
|
|
219
|
+
`?- p(4).\n false.\n\n` +
|
|
220
|
+
`?- X = 1.\n X = 2, unexpected.\n\n` +
|
|
221
|
+
`?- p(X).\n X = 1, ... .\n\n` +
|
|
222
|
+
`?- atom_length(1, L).\n type_error(atom, 1).\n\n` +
|
|
223
|
+
`?- atom_length(1, L).\n error(type_error(atom, 1), _).\n\n` +
|
|
224
|
+
`?- throw(ball).\n throw(ball).\n\n` +
|
|
225
|
+
`?- write(ok), nl.\n outputs("ok\\n"), true.\n\n` +
|
|
226
|
+
`?- get_char(C).\n inputs("a"), C = a.\n\n` +
|
|
227
|
+
`?- get_char(C).\n inputs("ab"), C = a, unexpected.\n\n` +
|
|
228
|
+
`?- X = 1.\n X = 2, unexpected.\n X = 1.\n\n` +
|
|
229
|
+
`?- catch(throw(ball), E, true).\n E = ball | error(system_error, ...).\n`;
|
|
230
|
+
const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'quads.pl' }]));
|
|
231
|
+
assertEqual(result.total, 12, 'quad total');
|
|
232
|
+
assertEqual(result.passed, 12, 'quad passed');
|
|
233
|
+
assertEqual(result.failed, 0, 'quad failed');
|
|
234
|
+
assertEqual(result.stdout, 'quads: 12 run, 12 passed, 0 failed.\n', 'quad report');
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'runQuads matches the corrected ISO phrase quad boundaries',
|
|
239
|
+
run: () => {
|
|
240
|
+
const source = String.raw`c2 ?- call((1,fail)).
|
|
241
|
+
type_error(callable,(1,fail)).
|
|
242
|
+
|
|
243
|
+
c3 ?- call((fail,1)).
|
|
244
|
+
type_error(callable,(fail,1)).
|
|
245
|
+
|
|
246
|
+
c4 ?- call((!;1)).
|
|
247
|
+
type_error(callable,(!;1)).
|
|
248
|
+
|
|
249
|
+
24 ?- asserta((a-->b)).
|
|
250
|
+
permission_error(modify,static_procedure,(-->)/2).
|
|
251
|
+
|
|
252
|
+
25 ?- clause((a-->b),B).
|
|
253
|
+
permission_error(access,private_procedure,(-->)/2).
|
|
254
|
+
|
|
255
|
+
26 ?- (X-->Y).
|
|
256
|
+
existence_error(procedure,(-->)/2).
|
|
257
|
+
|
|
258
|
+
5 ?- phrase([a|b],L).
|
|
259
|
+
type_error(list,[a|b]).
|
|
260
|
+
|
|
261
|
+
10 ?- phrase(([a],{1}),[]).
|
|
262
|
+
type_error(callable,(...,...)).
|
|
263
|
+
|
|
264
|
+
37 ?- phrase((!,[a],{1}),[]).
|
|
265
|
+
type_error(callable,(...,...)).
|
|
266
|
+
|
|
267
|
+
12 ?- phrase('|'([],[a]),[a]).
|
|
268
|
+
true.
|
|
269
|
+
|
|
270
|
+
14 ?- phrase(([a];[]),L).
|
|
271
|
+
L=[a] ; L=[].
|
|
272
|
+
|
|
273
|
+
15 ?- phrase({fail,1},L).
|
|
274
|
+
type_error(callable,((fail,1),...)).
|
|
275
|
+
|
|
276
|
+
29 ?- phrase(([a],\+1),[]).
|
|
277
|
+
false.
|
|
278
|
+
|
|
279
|
+
30 ?- phrase(([a],\+1;[]),[]).
|
|
280
|
+
true.
|
|
281
|
+
|
|
282
|
+
31 ?- phrase(phrase(phrase,[]),L).
|
|
283
|
+
existence_error(procedure,phrase/4).
|
|
284
|
+
|
|
285
|
+
32 ?- phrase(call([]),[]).
|
|
286
|
+
existence_error(procedure,[]/2).
|
|
287
|
+
|
|
288
|
+
41 ?- phrase([],non_list).
|
|
289
|
+
type_error(list,non_list).
|
|
290
|
+
|
|
291
|
+
42 ?- phrase([],[a|non_list]).
|
|
292
|
+
type_error(list,[a|non_list]).
|
|
293
|
+
|
|
294
|
+
43 ?- phrase([],L,non_list).
|
|
295
|
+
type_error(list,non_list).
|
|
296
|
+
|
|
297
|
+
44 ?- phrase([],L,[a|non_list]).
|
|
298
|
+
type_error(list,[a|non_list]).
|
|
299
|
+
|
|
300
|
+
46 ?- phrase((1,{2}),[]).
|
|
301
|
+
type_error(callable,1).
|
|
302
|
+
|
|
303
|
+
47 ?- phrase(({2},1),[]).
|
|
304
|
+
type_error(callable,1).
|
|
305
|
+
`;
|
|
306
|
+
const result = publicApi.runQuads(source);
|
|
307
|
+
assertEqual(result.total, 22, 'quad total');
|
|
308
|
+
assertEqual(result.passed, 22, 'quad passed');
|
|
309
|
+
assertEqual(result.stdout, 'quads: 22 run, 22 passed, 0 failed.\n', 'quad report');
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'runQuads rejects malformed answer substitutions',
|
|
314
|
+
run: () => {
|
|
315
|
+
const source = `?- X = f(Y), Y = 1.\n X = f(Y), Y = 1.\n`;
|
|
316
|
+
const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'malformed-quad.pl' }]));
|
|
317
|
+
assertEqual(result.total, 1, 'quad total');
|
|
318
|
+
assertEqual(result.failed, 1, 'quad failed');
|
|
319
|
+
assertIncludes(result.stdout, 'quads: MALFORMED malformed-quad.pl:1', 'malformed report');
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'runQuads reports annotations that cannot be checked safely',
|
|
324
|
+
run: () => {
|
|
325
|
+
const result = publicApi.runQuads(`?- repeat, fail.\n loops.\n`);
|
|
326
|
+
assertEqual(result.results[0].kind, 'unsupported', 'quad result');
|
|
327
|
+
assertIncludes(result.stdout, 'quads: UNSUPPORTED <input>:1', 'unsupported report');
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: '--quads runs embedded tests and reports failures through exit status',
|
|
332
|
+
run: () => {
|
|
333
|
+
const passing = runCli(['--quads', '-'], {
|
|
334
|
+
input: `p(ok).\n\nsmoke ?- p(X).\n X = ok.\n`,
|
|
335
|
+
});
|
|
336
|
+
assertEqual(passing.status, 0, 'passing quad exit status');
|
|
337
|
+
assertEqual(passing.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'passing quad stdout');
|
|
338
|
+
assertEqual(passing.stderr, '', 'passing quad stderr');
|
|
339
|
+
|
|
340
|
+
const failing = runCli(['-q', '-'], {
|
|
341
|
+
input: `p(actual).\n\nsmoke ?- p(X).\n X = expected.\n`,
|
|
342
|
+
});
|
|
343
|
+
assertEqual(failing.status, 1, 'failing quad exit status');
|
|
344
|
+
assertIncludes(failing.stdout, 'quads: FAILED smoke, <stdin>:3', 'failing quad report');
|
|
345
|
+
assertIncludes(failing.stdout, 'quads: 1 run, 0 passed, 1 failed.', 'failing quad summary');
|
|
346
|
+
assertEqual(failing.stderr, '', 'failing quad stderr');
|
|
347
|
+
},
|
|
348
|
+
},
|
|
186
349
|
{
|
|
187
350
|
name: 'seeded random/3 sequence is reproducible',
|
|
188
351
|
run: () => {
|
|
@@ -212,6 +375,7 @@ why(
|
|
|
212
375
|
assertIncludes(result.stdout, 'With no arguments, start a Prolog REPL.', 'stdout');
|
|
213
376
|
assertIncludes(result.stdout, '-g, --goal goal', 'stdout');
|
|
214
377
|
assertIncludes(result.stdout, '-p, --proof', 'stdout');
|
|
378
|
+
assertIncludes(result.stdout, '-q, --quads', 'stdout');
|
|
215
379
|
assertIncludes(result.stdout, '-s, --stats', 'stdout');
|
|
216
380
|
assertIncludes(result.stdout, '-v, --version', 'stdout');
|
|
217
381
|
assertIncludes(result.stdout, '-w, --warnings', 'stdout');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5341,8 +5341,8 @@ look_ahead(X), [X] --> [X].
|
|
|
5341
5341
|
`phrase(+Body,?Sequence)` accepts or generates a complete sequence.
|
|
5342
5342
|
`phrase(+Body,?Sequence,?Rest)` leaves `Rest` unconsumed and is steadfast in
|
|
5343
5343
|
that argument. A variable body raises `instantiation_error`; a non-callable
|
|
5344
|
-
body raises `type_error(callable)`. EyeProlog performs
|
|
5345
|
-
|
|
5344
|
+
body raises `type_error(callable)`. EyeProlog performs terminal-sequence checks
|
|
5345
|
+
and reports the portable ISO `type_error(list)` error term.
|
|
5346
5346
|
|
|
5347
5347
|
Part 3 leaves `\+//1` and standalone `->//2` implementation dependent.
|
|
5348
5348
|
EyeProlog uses non-consuming negation (`\+ Body` tests from the current state)
|
|
@@ -6072,13 +6072,14 @@ make the observed question explicit.
|
|
|
6072
6072
|
| --- | --- |
|
|
6073
6073
|
| `-h`, `--help` | Show usage |
|
|
6074
6074
|
| `-p`, `--proof` | Print `why/2` explanations |
|
|
6075
|
+
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
6075
6076
|
| `-s`, `--stats` | Print solver counters to stderr |
|
|
6076
6077
|
| `-v`, `--version` | Print the package version |
|
|
6077
6078
|
| `-w`, `--warnings` | Print non-fatal portability warnings |
|
|
6078
6079
|
| `-g`, `--goal Goal` | Solve a callable goal; may be repeated; overrides `%% goal:` comments |
|
|
6079
6080
|
| `--` | Treat following arguments as inputs |
|
|
6080
6081
|
|
|
6081
|
-
Short flags may be combined, so `-
|
|
6082
|
+
Short flags may be combined, so `-pqw` is equivalent to `-p -q -w`.
|
|
6082
6083
|
|
|
6083
6084
|
Inputs may be local files, HTTP(S) URLs, or one `-` for stdin. The bare command
|
|
6084
6085
|
`eyeprolog` starts the REPL. When options are present but no input is named,
|
|
@@ -6113,6 +6114,60 @@ they do not corrupt that logical stream. A successful run normally exits with
|
|
|
6113
6114
|
status zero; loading, syntax, option, and other uncaught errors use status `1`. `halt/0-1` can deliberately choose the
|
|
6114
6115
|
process status from inside a program.
|
|
6115
6116
|
|
|
6117
|
+
### Embedded quad tests
|
|
6118
|
+
|
|
6119
|
+
A quad places a query directly before a description of its expected top-level
|
|
6120
|
+
answer. The answer is ordinary Prolog syntax rather than quoted text or a
|
|
6121
|
+
comment, so a small test reads like the interaction it checks:
|
|
6122
|
+
|
|
6123
|
+
```eyeprolog
|
|
6124
|
+
color(red).
|
|
6125
|
+
color(green).
|
|
6126
|
+
|
|
6127
|
+
colors ?- color(X).
|
|
6128
|
+
X = red
|
|
6129
|
+
; X = green.
|
|
6130
|
+
|
|
6131
|
+
?- color(blue).
|
|
6132
|
+
false.
|
|
6133
|
+
```
|
|
6134
|
+
|
|
6135
|
+
Run all quads in a file with `eyeprolog --quads file.pl` or `eyeprolog -q
|
|
6136
|
+
file.pl`. A label such as `colors` is optional. Loading the file normally only
|
|
6137
|
+
records its quads; it does not execute them or add their queries and answers as
|
|
6138
|
+
program clauses. A quad run prints a summary and exits with status `1` when any
|
|
6139
|
+
description fails.
|
|
6140
|
+
|
|
6141
|
+
Unless the source explicitly selects another `unknown` flag, quad execution
|
|
6142
|
+
uses `unknown=error`, so an undefined predicate is reported rather than being
|
|
6143
|
+
accepted as a negative answer.
|
|
6144
|
+
|
|
6145
|
+
Answer descriptions support ordered answers separated by `;`, acceptable
|
|
6146
|
+
alternatives separated by `|`, `true`, `false`, standard error descriptions,
|
|
6147
|
+
and the `unexpected` annotation for an answer that must not occur (`inattendue`
|
|
6148
|
+
is its synonym). `...` and `ad_infinitum` accept further answers. Multiple
|
|
6149
|
+
indented descriptions after one query must all hold. `inputs/1` supplies and
|
|
6150
|
+
checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
|
|
6151
|
+
an answer description that this finite-tree implementation skips. The
|
|
6152
|
+
nontermination and advanced stream annotations `loops`, `peeks/1`, and `waits`,
|
|
6153
|
+
and the unordered `other_answer_sequence` annotation, are not executed by the
|
|
6154
|
+
current runner.
|
|
6155
|
+
|
|
6156
|
+
The JavaScript API exposes the same operation without process I/O:
|
|
6157
|
+
|
|
6158
|
+
```js
|
|
6159
|
+
import { Program, runQuads } from 'eyeprolog';
|
|
6160
|
+
|
|
6161
|
+
const program = Program.parse(source);
|
|
6162
|
+
const report = runQuads(program);
|
|
6163
|
+
console.log(report.passed, report.failed, report.stdout);
|
|
6164
|
+
```
|
|
6165
|
+
|
|
6166
|
+
The syntax follows the “queries using answer descriptions” convention used by
|
|
6167
|
+
Trealla and the ISO Prolog working examples. Because answer descriptions are
|
|
6168
|
+
layout-sensitive, indent every description while keeping ordinary clause heads
|
|
6169
|
+
and the next quad query at the left margin.
|
|
6170
|
+
|
|
6116
6171
|
Statistics are comparative evidence, not a score in isolation. Preserve the
|
|
6117
6172
|
program, input, runtime version, selected query, answers, and counters together.
|
|
6118
6173
|
An optimization is acceptable only when the intended answers remain unchanged
|