eyeprolog 1.5.20 → 1.5.22
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/index.d.ts +52 -2
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +6 -0
- package/src/cli.js +42 -6
- package/src/explain.js +315 -15
- package/src/index.js +4 -4
- package/src/write.js +29 -11
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +1 -1
- package/test/conformance/expected/iso/logtalk_univ.pl +1 -1
- package/test/run-regression.mjs +196 -2
- package/the-art-of-eyeprolog.md +128 -11
- package/why-eyeprolog.md +14 -3
package/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface EyePrologRunOptions {
|
|
|
12
12
|
/** Host-supplied goals, executed in order. */
|
|
13
13
|
goals?: Array<string | EyePrologTerm>;
|
|
14
14
|
proof?: boolean;
|
|
15
|
+
/** Proof detail: abstract keeps library predicates as trusted leaves; expanded opens bundled Prolog library clauses. */
|
|
16
|
+
proofDetail?: 'abstract' | 'expanded';
|
|
15
17
|
why?: boolean;
|
|
16
18
|
explain?: boolean;
|
|
17
19
|
maxDepth?: number;
|
|
@@ -310,9 +312,54 @@ export function hasForwardRules(program: Program): boolean;
|
|
|
310
312
|
/** Execute EyeProlog `:+/2` rules to closure using an existing solver. */
|
|
311
313
|
export function executeForwardRules(program: Program, solver: Solver, options?: EyePrologForwardRunOptions): EyePrologForwardRunResult;
|
|
312
314
|
export function runQuads(source: string | Program, options?: EyePrologQuadRunOptions): EyePrologQuadRunResult;
|
|
313
|
-
export
|
|
315
|
+
export interface EyePrologProofMethod {
|
|
316
|
+
type: 'source' | 'builtin' | 'library' | 'conjunction';
|
|
317
|
+
kind?: 'fact' | 'rule';
|
|
318
|
+
filename?: string;
|
|
319
|
+
clause?: number;
|
|
320
|
+
name?: string;
|
|
321
|
+
arity?: number;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface EyePrologProofNode {
|
|
325
|
+
goal: string;
|
|
326
|
+
method: EyePrologProofMethod;
|
|
327
|
+
bindings: Array<{ name: string; value: string }>;
|
|
328
|
+
children: EyePrologProofNode[];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface EyePrologProofCertificate {
|
|
332
|
+
version: 1;
|
|
333
|
+
detail: 'abstract' | 'expanded';
|
|
334
|
+
answer: string;
|
|
335
|
+
proof: EyePrologProofNode;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface EyePrologProofResult {
|
|
339
|
+
ok: boolean;
|
|
340
|
+
certificate: EyePrologProofCertificate | null;
|
|
341
|
+
text: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface EyePrologProofTrustBoundary {
|
|
345
|
+
type: 'builtin' | 'library';
|
|
346
|
+
name: string;
|
|
347
|
+
arity: number;
|
|
348
|
+
goal: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface EyePrologProofVerification {
|
|
352
|
+
ok: boolean;
|
|
353
|
+
error: string | null;
|
|
354
|
+
trusted: EyePrologProofTrustBoundary[];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function proofCertificate(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): EyePrologProofResult;
|
|
358
|
+
export function proofCertificatesFromText(text: string, program: Program): EyePrologProofCertificate[];
|
|
359
|
+
export function verifyProof(program: Program, certificate: EyePrologProofCertificate | EyePrologProofResult, options?: EyePrologRunOptions): EyePrologProofVerification;
|
|
360
|
+
export function whyProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): EyePrologProofResult;
|
|
314
361
|
export function whyNoProof(goal: EyePrologTerm): string;
|
|
315
|
-
export function explainProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions):
|
|
362
|
+
export function explainProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): EyePrologProofResult;
|
|
316
363
|
|
|
317
364
|
declare const eyeprolog: {
|
|
318
365
|
VAR: typeof VAR;
|
|
@@ -386,6 +433,9 @@ declare const eyeprolog: {
|
|
|
386
433
|
hasForwardRules: typeof hasForwardRules;
|
|
387
434
|
executeForwardRules: typeof executeForwardRules;
|
|
388
435
|
runQuads: typeof runQuads;
|
|
436
|
+
proofCertificate: typeof proofCertificate;
|
|
437
|
+
proofCertificatesFromText: typeof proofCertificatesFromText;
|
|
438
|
+
verifyProof: typeof verifyProof;
|
|
389
439
|
whyProof: typeof whyProof;
|
|
390
440
|
whyNoProof: typeof whyNoProof;
|
|
391
441
|
explainProof: typeof explainProof;
|
package/package.json
CHANGED
package/src/ARCHITECTURE.md
CHANGED
|
@@ -21,6 +21,12 @@ higher-level frontends.
|
|
|
21
21
|
5. **Frontends/tools** — `execute.js`, `repl.js`, `cli.js`, `quads.js`,
|
|
22
22
|
`explain.js`, and the playground worker.
|
|
23
23
|
|
|
24
|
+
`explain.js` owns both proof construction and proof-certificate verification.
|
|
25
|
+
Verification walks the supplied certificate and checks source-clause steps against
|
|
26
|
+
the parsed `Program`; it does not call the solver to rediscover the proof.
|
|
27
|
+
Built-in and abstract library nodes are explicit trust boundaries, while expanded
|
|
28
|
+
proofs expose bundled Prolog-library clauses as ordinary source steps.
|
|
29
|
+
|
|
24
30
|
`iso.js` and `program.js` remain facade modules for their existing exports, so
|
|
25
31
|
this refactor does not change the public JavaScript API.
|
|
26
32
|
|
package/src/cli.js
CHANGED
|
@@ -25,6 +25,8 @@ export async function main(argv) {
|
|
|
25
25
|
const options = {
|
|
26
26
|
files: [],
|
|
27
27
|
proof: false,
|
|
28
|
+
proofDetail: 'abstract',
|
|
29
|
+
verifyProof: null,
|
|
28
30
|
quads: false,
|
|
29
31
|
stats: false,
|
|
30
32
|
isoStrict: false,
|
|
@@ -47,6 +49,15 @@ export async function main(argv) {
|
|
|
47
49
|
return;
|
|
48
50
|
} else if (!endOptions && (arg === '--proof' || arg === '-p')) {
|
|
49
51
|
options.proof = true;
|
|
52
|
+
} else if (!endOptions && arg === '--proof-detail') {
|
|
53
|
+
const detail = argv[++i];
|
|
54
|
+
if (detail !== 'abstract' && detail !== 'expanded') throw new Error('--proof-detail requires abstract or expanded');
|
|
55
|
+
options.proof = true;
|
|
56
|
+
options.proofDetail = detail;
|
|
57
|
+
} else if (!endOptions && arg === '--verify-proof') {
|
|
58
|
+
const file = argv[++i];
|
|
59
|
+
if (file == null) throw new Error('--verify-proof requires a file');
|
|
60
|
+
options.verifyProof = file;
|
|
50
61
|
} else if (!endOptions && (arg === '--quads' || arg === '-q')) {
|
|
51
62
|
options.quads = true;
|
|
52
63
|
} else if (!endOptions && (arg === '--stats' || arg === '-s')) {
|
|
@@ -94,9 +105,18 @@ export async function main(argv) {
|
|
|
94
105
|
if (options.isoStrict && options.quads) {
|
|
95
106
|
throw new Error('--iso-strict cannot be combined with --quads');
|
|
96
107
|
}
|
|
108
|
+
if (options.verifyProof != null && options.quads) {
|
|
109
|
+
throw new Error('--verify-proof cannot be combined with --quads');
|
|
110
|
+
}
|
|
111
|
+
if (options.verifyProof != null && options.proof) {
|
|
112
|
+
throw new Error('--verify-proof cannot be combined with --proof or --proof-detail');
|
|
113
|
+
}
|
|
114
|
+
if (options.verifyProof != null && options.goals.length > 0) {
|
|
115
|
+
throw new Error('--verify-proof cannot be combined with --goal');
|
|
116
|
+
}
|
|
97
117
|
|
|
98
118
|
if (options.isoStrict && options.files.length === 0 && options.goals.length === 0 &&
|
|
99
|
-
!options.proof && !options.stats && !options.warnings) {
|
|
119
|
+
options.verifyProof == null && !options.proof && !options.stats && !options.warnings) {
|
|
100
120
|
const engine = await loadEngine();
|
|
101
121
|
const { runRepl } = await import('./repl.js');
|
|
102
122
|
const exitCode = await runRepl(engine, {
|
|
@@ -134,7 +154,7 @@ export async function main(argv) {
|
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
156
|
|
|
137
|
-
if (options.goals.length === 0 && !options.quads) {
|
|
157
|
+
if (options.goals.length === 0 && !options.quads && options.verifyProof == null) {
|
|
138
158
|
for (const source of sourceParts) options.goals.push(...goalsFromSource(source.text));
|
|
139
159
|
}
|
|
140
160
|
|
|
@@ -150,7 +170,7 @@ export async function main(argv) {
|
|
|
150
170
|
|
|
151
171
|
const engine = await loadEngine();
|
|
152
172
|
let program = engine.Program.parseSources(sourceParts, {
|
|
153
|
-
sourceMetadata: options.proof || options.isoStrict,
|
|
173
|
+
sourceMetadata: options.proof || options.verifyProof != null || options.isoStrict,
|
|
154
174
|
isoStrict: options.isoStrict,
|
|
155
175
|
autoload: options.autoload,
|
|
156
176
|
autoloadGoals: options.goals,
|
|
@@ -164,6 +184,20 @@ export async function main(argv) {
|
|
|
164
184
|
return;
|
|
165
185
|
}
|
|
166
186
|
|
|
187
|
+
if (options.verifyProof != null) {
|
|
188
|
+
const explanation = await loadExplanation();
|
|
189
|
+
const proofText = await fs.readFile(options.verifyProof, 'utf8');
|
|
190
|
+
const certificates = explanation.proofCertificatesFromText(proofText, program);
|
|
191
|
+
if (certificates.length === 0) throw new Error(`no why/2 proof certificate found in ${options.verifyProof}`);
|
|
192
|
+
const registry = options.isoStrict ? engine.getStrictIsoRegistry() : engine.getEyePrologRegistry();
|
|
193
|
+
for (let i = 0; i < certificates.length; i++) {
|
|
194
|
+
const checked = explanation.verifyProof(program, certificates[i], { registry });
|
|
195
|
+
if (!checked.ok) throw new Error(`proof certificate ${i + 1} failed verification: ${checked.error}`);
|
|
196
|
+
}
|
|
197
|
+
process.stdout.write(`verified ${certificates.length} proof certificate${certificates.length === 1 ? '' : 's'}.\n`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
167
201
|
if (!options.quads || options.goals.length > 0) {
|
|
168
202
|
if (options.goals.length === 0 && !options.isoStrict && engine.hasForwardRules(program)) {
|
|
169
203
|
await runForwardDefault(engine, program, options);
|
|
@@ -244,7 +278,7 @@ async function runDefault(engine, program, options) {
|
|
|
244
278
|
const { haltCode } = engine.executeGoals(program, solver, goals, {
|
|
245
279
|
onAnswer: (line, resolved) => {
|
|
246
280
|
process.stdout.write(line);
|
|
247
|
-
if (options.proof) writeExplanation(explanation, program, resolved, registry);
|
|
281
|
+
if (options.proof) writeExplanation(explanation, program, resolved, registry, options);
|
|
248
282
|
},
|
|
249
283
|
});
|
|
250
284
|
if (haltCode != null) process.exitCode = haltCode;
|
|
@@ -253,8 +287,8 @@ async function runDefault(engine, program, options) {
|
|
|
253
287
|
}
|
|
254
288
|
}
|
|
255
289
|
|
|
256
|
-
function writeExplanation(explanation, program, resolved, registry) {
|
|
257
|
-
const proof = explanation.whyProof(program, resolved, { registry });
|
|
290
|
+
function writeExplanation(explanation, program, resolved, registry, options = {}) {
|
|
291
|
+
const proof = explanation.whyProof(program, resolved, { registry, proofDetail: options?.proofDetail ?? 'abstract' });
|
|
258
292
|
process.stdout.write(proof.text);
|
|
259
293
|
if (!proof.ok) process.stdout.write(explanation.whyNoProof(resolved));
|
|
260
294
|
}
|
|
@@ -276,6 +310,8 @@ Input:
|
|
|
276
310
|
Options:
|
|
277
311
|
-h, --help Show this help text and exit.
|
|
278
312
|
-p, --proof Enable proof explanations.
|
|
313
|
+
--proof-detail mode Use abstract or expanded proof detail (implies --proof).
|
|
314
|
+
--verify-proof file Verify why/2 proof certificates against the input program.
|
|
279
315
|
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
280
316
|
-s, --stats Print solver and memory statistics to stderr after execution.
|
|
281
317
|
--iso-strict Use ISO/IEC 13211-1 core + Corrigenda 1-3 only;
|
package/src/explain.js
CHANGED
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
// The explanation printer replays a successful goal against the program and emits
|
|
3
3
|
// ordinary EyeProlog facts with nested proof terms. Explanations are therefore both
|
|
4
4
|
// human-readable and machine-readable.
|
|
5
|
-
import { ATOM, COMPOUND, Env, Term, VAR, deref, flattenConjunction, freshTerm, termToString, unify, variantTerms } from './term.js';
|
|
5
|
+
import { ATOM, COMPOUND, Env, Term, VAR, deref, flattenConjunction, freshTerm, properListItems, termToString, unify, variantTerms } from './term.js';
|
|
6
6
|
import { selectClauseCandidates } from './program.js';
|
|
7
|
+
import { parseGoalText, parseProgramText } from './parser.js';
|
|
7
8
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
8
9
|
import { Solver, nextFreshId } from './solver.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
let verifyFreshCounter = 0;
|
|
12
|
+
|
|
13
|
+
export function proofCertificate(program, goal, options = {}) {
|
|
11
14
|
const maxDepth = options.maxDepth ?? 256;
|
|
12
15
|
const registry = options.registry ?? getEyePrologRegistry();
|
|
13
16
|
const env = options.env ?? new Env();
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const detail = normalizeProofDetail(options.proofDetail ?? 'abstract');
|
|
18
|
+
for (const proof of proveGoalAll(program, goal, env, 0, maxDepth, registry, [], detail)) {
|
|
19
|
+
const answer = resolveForProof(goal, proof.env);
|
|
20
|
+
const certificate = {
|
|
21
|
+
version: 1,
|
|
22
|
+
detail,
|
|
23
|
+
answer: termToString(answer, new Env(), true),
|
|
24
|
+
proof: certificateNode(proof.node),
|
|
25
|
+
};
|
|
26
|
+
return { ok: true, certificate, text: renderWhyFacts(goal, proof.node, proof.env) };
|
|
16
27
|
}
|
|
17
|
-
return { ok: false, text: '' };
|
|
28
|
+
return { ok: false, certificate: null, text: '' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function whyProof(program, goal, options = {}) {
|
|
32
|
+
return proofCertificate(program, goal, options);
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
export function whyNoProof(goal) {
|
|
@@ -26,11 +41,11 @@ export function explainProof(program, goal, options = {}) {
|
|
|
26
41
|
return whyProof(program, goal, options);
|
|
27
42
|
}
|
|
28
43
|
|
|
29
|
-
function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active) {
|
|
44
|
+
function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, detail) {
|
|
30
45
|
if (depth > maxDepth) return;
|
|
31
46
|
|
|
32
47
|
if (goal.type === COMPOUND && goal.name === ',' && goal.arity === 2) {
|
|
33
|
-
for (const proved of proveGoalsAll(program, flattenConjunction(goal), env, depth + 1, maxDepth, registry, active)) {
|
|
48
|
+
for (const proved of proveGoalsAll(program, flattenConjunction(goal), env, depth + 1, maxDepth, registry, active, detail)) {
|
|
34
49
|
yield {
|
|
35
50
|
env: proved.env,
|
|
36
51
|
node: {
|
|
@@ -58,7 +73,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active) {
|
|
|
58
73
|
sourceHead: resolveForProof(goal, proofEnv),
|
|
59
74
|
sourceBody: [],
|
|
60
75
|
bindings: [],
|
|
61
|
-
children: builtinChildren(program, goal, proofEnv, depth + 1, maxDepth, registry, active),
|
|
76
|
+
children: builtinChildren(program, goal, proofEnv, depth + 1, maxDepth, registry, active, detail),
|
|
62
77
|
},
|
|
63
78
|
};
|
|
64
79
|
}
|
|
@@ -74,7 +89,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active) {
|
|
|
74
89
|
// a standard Prolog module. The implementation remains
|
|
75
90
|
// ordinary clauses, but explanations collapse its private helper expansion
|
|
76
91
|
// behind an explicit library(Name, Arity) boundary.
|
|
77
|
-
if (group.module !== 'user' && program.modules.get(group.module)?.filename?.startsWith('src/lib/')) {
|
|
92
|
+
if (detail !== 'expanded' && group.module !== 'user' && program.modules.get(group.module)?.filename?.startsWith('src/lib/')) {
|
|
78
93
|
const solver = new Solver(program, { registry });
|
|
79
94
|
for (const next of solver.solve([goal], env.clone(), 0)) {
|
|
80
95
|
const proofEnv = next.clone ? next.clone() : next;
|
|
@@ -128,7 +143,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active) {
|
|
|
128
143
|
let activePushed = true;
|
|
129
144
|
active.push({ goal, env });
|
|
130
145
|
try {
|
|
131
|
-
for (const proved of proveGoalsAll(program, freshBody, next, depth + 1, maxDepth, registry, active)) {
|
|
146
|
+
for (const proved of proveGoalsAll(program, freshBody, next, depth + 1, maxDepth, registry, active, detail)) {
|
|
132
147
|
active.pop();
|
|
133
148
|
activePushed = false;
|
|
134
149
|
yield {
|
|
@@ -160,7 +175,7 @@ function clauseCandidateAt(candidate, index) {
|
|
|
160
175
|
return Array.isArray(candidate) ? candidate[index] : index === 0 ? candidate : undefined;
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
function* proveGoalsAll(program, goals, env, depth, maxDepth, registry, active) {
|
|
178
|
+
function* proveGoalsAll(program, goals, env, depth, maxDepth, registry, active, detail) {
|
|
164
179
|
if (goals.length === 0) {
|
|
165
180
|
yield { env: env.clone(), children: [] };
|
|
166
181
|
return;
|
|
@@ -170,8 +185,8 @@ function* proveGoalsAll(program, goals, env, depth, maxDepth, registry, active)
|
|
|
170
185
|
const goal = goals[selectedIndex];
|
|
171
186
|
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
172
187
|
|
|
173
|
-
for (const proved of proveGoalAll(program, goal, env, depth, maxDepth, registry, active)) {
|
|
174
|
-
for (const tail of proveGoalsAll(program, rest, proved.env, depth, maxDepth, registry, active)) {
|
|
188
|
+
for (const proved of proveGoalAll(program, goal, env, depth, maxDepth, registry, active, detail)) {
|
|
189
|
+
for (const tail of proveGoalsAll(program, rest, proved.env, depth, maxDepth, registry, active, detail)) {
|
|
175
190
|
const children = tail.children.slice();
|
|
176
191
|
children.splice(selectedIndex, 0, proved.node);
|
|
177
192
|
yield { env: tail.env, children };
|
|
@@ -212,10 +227,10 @@ function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
|
212
227
|
return 0;
|
|
213
228
|
}
|
|
214
229
|
|
|
215
|
-
function builtinChildren(program, goal, env, depth, maxDepth, registry, active) {
|
|
230
|
+
function builtinChildren(program, goal, env, depth, maxDepth, registry, active, detail) {
|
|
216
231
|
if (goal.type !== COMPOUND) return [];
|
|
217
232
|
if (goal.name === 'once' && goal.arity === 1) {
|
|
218
|
-
for (const proved of proveGoalAll(program, goal.args[0], env.clone(), depth, maxDepth, registry, active)) return [proved.node];
|
|
233
|
+
for (const proved of proveGoalAll(program, goal.args[0], env.clone(), depth, maxDepth, registry, active, detail)) return [proved.node];
|
|
219
234
|
}
|
|
220
235
|
return [];
|
|
221
236
|
}
|
|
@@ -250,6 +265,291 @@ function libraryMethod(goal) {
|
|
|
250
265
|
};
|
|
251
266
|
}
|
|
252
267
|
|
|
268
|
+
function normalizeProofDetail(value) {
|
|
269
|
+
if (value === 'abstract' || value === 'expanded') return value;
|
|
270
|
+
throw new Error(`unknown proof detail: ${value}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function certificateNode(node) {
|
|
274
|
+
return {
|
|
275
|
+
goal: termToString(node.goal, new Env(), true),
|
|
276
|
+
method: certificateMethod(node.method),
|
|
277
|
+
bindings: node.bindings.map((binding) => ({
|
|
278
|
+
name: String(binding.name),
|
|
279
|
+
value: termToString(binding.value, new Env(), true),
|
|
280
|
+
})),
|
|
281
|
+
children: node.children.map(certificateNode),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function certificateMethod(method) {
|
|
286
|
+
if (typeof method === 'string') return { type: method };
|
|
287
|
+
if (!method || typeof method !== 'object') return { type: String(method) };
|
|
288
|
+
return { ...method };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function proofCertificatesFromText(text, program) {
|
|
292
|
+
const clauses = parseProgramText(String(text), {
|
|
293
|
+
doubleQuotes: program.doubleQuotes ?? 'chars',
|
|
294
|
+
sourceMetadata: false,
|
|
295
|
+
});
|
|
296
|
+
const certificates = [];
|
|
297
|
+
for (const clause of clauses) {
|
|
298
|
+
if (!clause?.head || clause.body?.length !== 0 || clause.head.type !== COMPOUND ||
|
|
299
|
+
clause.head.name !== 'why' || clause.head.arity !== 2) continue;
|
|
300
|
+
const proof = certificateNodeFromTerm(clause.head.args[1]);
|
|
301
|
+
if (!proof) continue;
|
|
302
|
+
certificates.push({
|
|
303
|
+
version: 1,
|
|
304
|
+
detail: containsExpandedLibrarySource(proof) ? 'expanded' : 'abstract',
|
|
305
|
+
answer: termToString(clause.head.args[0], new Env(), true),
|
|
306
|
+
proof,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return certificates;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function certificateNodeFromTerm(term) {
|
|
313
|
+
if (term.type !== COMPOUND || term.name !== 'proof') return null;
|
|
314
|
+
const fields = term.args;
|
|
315
|
+
if (fields.length < 2) return null;
|
|
316
|
+
const goalField = fields[0];
|
|
317
|
+
const byField = fields[1];
|
|
318
|
+
if (goalField.type !== COMPOUND || goalField.name !== 'goal' || goalField.arity !== 1) return null;
|
|
319
|
+
if (byField.type !== COMPOUND || byField.name !== 'by' || byField.arity !== 1) return null;
|
|
320
|
+
const node = {
|
|
321
|
+
goal: termToString(goalField.args[0], new Env(), true),
|
|
322
|
+
method: certificateMethodFromTerm(byField.args[0]),
|
|
323
|
+
bindings: [],
|
|
324
|
+
children: [],
|
|
325
|
+
};
|
|
326
|
+
if (!node.method) return null;
|
|
327
|
+
for (let i = 2; i < fields.length; i++) {
|
|
328
|
+
const field = fields[i];
|
|
329
|
+
if (field.type !== COMPOUND) return null;
|
|
330
|
+
if (field.name === 'bindings' && field.arity === 1) {
|
|
331
|
+
const items = properListItems(field.args[0], new Env());
|
|
332
|
+
if (items == null) return null;
|
|
333
|
+
node.bindings = items.map((item) => {
|
|
334
|
+
if (item.type !== COMPOUND || item.name !== 'binding' || item.arity !== 2) throw new Error('malformed certificate binding');
|
|
335
|
+
return { name: certificateText(item.args[0]), value: termToString(item.args[1], new Env(), true) };
|
|
336
|
+
});
|
|
337
|
+
} else if (field.name === 'uses' && field.arity === 1) {
|
|
338
|
+
const items = properListItems(field.args[0], new Env());
|
|
339
|
+
if (items == null) return null;
|
|
340
|
+
node.children = items.map(certificateNodeFromTerm);
|
|
341
|
+
if (node.children.some((child) => child == null)) return null;
|
|
342
|
+
} else {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return node;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function certificateMethodFromTerm(term) {
|
|
350
|
+
if (term.type === ATOM && term.name === 'conjunction') return { type: 'conjunction' };
|
|
351
|
+
if (term.type !== COMPOUND) return null;
|
|
352
|
+
if ((term.name === 'fact' || term.name === 'rule') && term.arity === 2) {
|
|
353
|
+
const clause = term.args[1];
|
|
354
|
+
if (clause.type !== COMPOUND || clause.name !== 'clause' || clause.arity !== 1) return null;
|
|
355
|
+
return { type: 'source', kind: term.name, filename: certificateText(term.args[0]), clause: Number(clause.args[0].name) };
|
|
356
|
+
}
|
|
357
|
+
if ((term.name === 'builtin' || term.name === 'library') && term.arity === 2) {
|
|
358
|
+
return { type: term.name, name: String(term.args[0].name), arity: Number(term.args[1].name) };
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function certificateText(term) {
|
|
364
|
+
if (term.type === ATOM || term.type === 'string') return String(term.name);
|
|
365
|
+
const items = properListItems(term, new Env());
|
|
366
|
+
if (items != null && items.every((item) => item.type === ATOM && String(item.name).length === 1)) {
|
|
367
|
+
return items.map((item) => String(item.name)).join('');
|
|
368
|
+
}
|
|
369
|
+
if (items != null && items.every((item) => item.type === 'number' && /^\d+$/.test(String(item.name)))) {
|
|
370
|
+
const codes = items.map((item) => Number(item.name));
|
|
371
|
+
if (codes.every((code) => Number.isInteger(code) && code >= 0 && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff))) {
|
|
372
|
+
return String.fromCodePoint(...codes);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
throw new Error('expected certificate text');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function containsLibraryBoundary(node) {
|
|
379
|
+
if (node.method?.type === 'library') return true;
|
|
380
|
+
return node.children.some(containsLibraryBoundary);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function containsExpandedLibrarySource(node) {
|
|
384
|
+
if (node.method?.type === 'source' && String(node.method.filename ?? '').startsWith('src/lib/')) return true;
|
|
385
|
+
return node.children.some(containsExpandedLibrarySource);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function verifyProof(program, input, options = {}) {
|
|
389
|
+
const certificate = input?.certificate ?? input;
|
|
390
|
+
try {
|
|
391
|
+
if (!certificate || certificate.version !== 1 || typeof certificate.answer !== 'string' || !certificate.proof ||
|
|
392
|
+
(certificate.detail !== 'abstract' && certificate.detail !== 'expanded')) {
|
|
393
|
+
throw new Error('invalid proof certificate');
|
|
394
|
+
}
|
|
395
|
+
if (certificate.detail === 'abstract' && containsExpandedLibrarySource(certificate.proof)) {
|
|
396
|
+
throw new Error('abstract proof certificate contains expanded library source');
|
|
397
|
+
}
|
|
398
|
+
if (certificate.detail === 'expanded' && containsLibraryBoundary(certificate.proof)) {
|
|
399
|
+
throw new Error('expanded proof certificate contains an abstract library boundary');
|
|
400
|
+
}
|
|
401
|
+
const registry = options.registry ?? getEyePrologRegistry();
|
|
402
|
+
const answer = parseCertificateTerm(certificate.answer, program);
|
|
403
|
+
const rootGoal = parseCertificateTerm(certificate.proof.goal, program);
|
|
404
|
+
const answerEnv = new Env();
|
|
405
|
+
if (!unify(answer, rootGoal, answerEnv)) throw new Error('certificate answer does not match root goal');
|
|
406
|
+
verifyCertificateNode(program, certificate.proof, rootGoal, registry, options, new Env());
|
|
407
|
+
return { ok: true, error: null, trusted: collectTrustedBoundaries(certificate.proof) };
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error), trusted: [] };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function collectTrustedBoundaries(node, out = []) {
|
|
414
|
+
if (node.method?.type === 'builtin' || node.method?.type === 'library') {
|
|
415
|
+
out.push({
|
|
416
|
+
type: node.method.type,
|
|
417
|
+
name: String(node.method.name),
|
|
418
|
+
arity: Number(node.method.arity),
|
|
419
|
+
goal: String(node.goal),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
for (const child of node.children) collectTrustedBoundaries(child, out);
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function parseCertificateTerm(text, program) {
|
|
427
|
+
return parseGoalText(String(text), { doubleQuotes: program.doubleQuotes ?? 'chars' });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function verifyCertificateNode(program, node, expectedGoal, registry, options, inheritedEnv) {
|
|
431
|
+
if (!node || typeof node.goal !== 'string' || !node.method || !Array.isArray(node.children) || !Array.isArray(node.bindings)) {
|
|
432
|
+
throw new Error('malformed proof node');
|
|
433
|
+
}
|
|
434
|
+
const nodeGoal = parseCertificateTerm(node.goal, program);
|
|
435
|
+
if (!unify(expectedGoal, nodeGoal, inheritedEnv)) throw new Error(`proof goal mismatch: ${node.goal}`);
|
|
436
|
+
const method = node.method;
|
|
437
|
+
|
|
438
|
+
if (method.type === 'conjunction') {
|
|
439
|
+
if (nodeGoal.type !== COMPOUND || nodeGoal.name !== ',' || nodeGoal.arity !== 2) throw new Error('invalid conjunction proof');
|
|
440
|
+
const goals = flattenConjunction(nodeGoal);
|
|
441
|
+
if (goals.length !== node.children.length) throw new Error('conjunction child count mismatch');
|
|
442
|
+
for (let i = 0; i < goals.length; i++) {
|
|
443
|
+
const childGoal = parseCertificateTerm(node.children[i].goal, program);
|
|
444
|
+
if (!unify(goals[i], childGoal, inheritedEnv)) throw new Error(`conjunction child ${i + 1} does not match`);
|
|
445
|
+
verifyCertificateNode(program, node.children[i], childGoal, registry, options, new Env());
|
|
446
|
+
}
|
|
447
|
+
if (node.bindings.length !== 0) throw new Error('conjunction proof must not carry clause bindings');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (method.type === 'builtin') {
|
|
452
|
+
const name = nodeGoal.type === COMPOUND || nodeGoal.type === ATOM ? nodeGoal.name : null;
|
|
453
|
+
const arity = nodeGoal.type === COMPOUND ? nodeGoal.arity : 0;
|
|
454
|
+
if (method.name !== name || method.arity !== arity || registry.get(name, arity) == null) {
|
|
455
|
+
throw new Error(`untrusted builtin boundary: ${method.name}/${method.arity}`);
|
|
456
|
+
}
|
|
457
|
+
if (node.bindings.length !== 0) throw new Error('builtin proof must not carry clause bindings');
|
|
458
|
+
if (name === 'once' && arity === 1) {
|
|
459
|
+
if (node.children.length !== 1) throw new Error('once/1 proof requires exactly one child');
|
|
460
|
+
const childGoal = parseCertificateTerm(node.children[0].goal, program);
|
|
461
|
+
const innerEnv = new Env();
|
|
462
|
+
if (!unify(nodeGoal.args[0], childGoal, innerEnv)) throw new Error('once/1 child does not match called goal');
|
|
463
|
+
verifyCertificateNode(program, node.children[0], childGoal, registry, options, new Env());
|
|
464
|
+
} else if (node.children.length !== 0) {
|
|
465
|
+
throw new Error(`unexpected children for builtin ${name}/${arity}`);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (method.type === 'library') {
|
|
471
|
+
const name = nodeGoal.type === COMPOUND || nodeGoal.type === ATOM ? nodeGoal.name : null;
|
|
472
|
+
const arity = nodeGoal.type === COMPOUND ? nodeGoal.arity : 0;
|
|
473
|
+
if (method.name !== name || method.arity !== arity) throw new Error('library boundary does not match goal');
|
|
474
|
+
const group = findLibraryGroup(program, name, arity);
|
|
475
|
+
if (!group) throw new Error(`untrusted library boundary: ${name}/${arity}`);
|
|
476
|
+
if (node.children.length !== 0 || node.bindings.length !== 0) throw new Error('abstract library proof must be a leaf');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (method.type === 'source') {
|
|
481
|
+
verifySourceNode(program, node, nodeGoal, registry, options);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
throw new Error(`unknown proof method: ${method.type}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function findLibraryGroup(program, name, arity) {
|
|
489
|
+
for (const group of program.groups.values()) {
|
|
490
|
+
if (group.name !== name || group.arity !== arity || group.module === 'user') continue;
|
|
491
|
+
if (program.modules.get(group.module)?.filename?.startsWith('src/lib/')) return group;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function verifySourceNode(program, node, nodeGoal, registry, options) {
|
|
497
|
+
const method = node.method;
|
|
498
|
+
const candidates = program.clauses.filter((clause) => {
|
|
499
|
+
const source = clause.source ?? {};
|
|
500
|
+
const filename = source.filename ?? '<input>';
|
|
501
|
+
const clauseNumber = source.clause ?? ((clause.index ?? 0) + 1);
|
|
502
|
+
return filename === method.filename && clauseNumber === method.clause;
|
|
503
|
+
});
|
|
504
|
+
if (candidates.length === 0) throw new Error(`source clause not found: ${method.filename}:${method.clause}`);
|
|
505
|
+
|
|
506
|
+
for (const clause of candidates) {
|
|
507
|
+
const kind = clause.body.length === 0 ? 'fact' : 'rule';
|
|
508
|
+
if (method.kind !== kind) continue;
|
|
509
|
+
const id = `verify${++verifyFreshCounter}`;
|
|
510
|
+
const variables = new Map();
|
|
511
|
+
const head = freshTerm(clause.head, id, variables);
|
|
512
|
+
const body = clause.body.map((term) => freshTerm(term, id, variables));
|
|
513
|
+
const env = new Env();
|
|
514
|
+
if (!unify(head, nodeGoal, env)) continue;
|
|
515
|
+
if (body.length !== node.children.length) continue;
|
|
516
|
+
|
|
517
|
+
let valid = true;
|
|
518
|
+
for (let i = 0; i < body.length; i++) {
|
|
519
|
+
try {
|
|
520
|
+
const childGoal = parseCertificateTerm(node.children[i].goal, program);
|
|
521
|
+
if (!unify(body[i], childGoal, env)) { valid = false; break; }
|
|
522
|
+
verifyCertificateNode(program, node.children[i], childGoal, registry, options, new Env());
|
|
523
|
+
} catch {
|
|
524
|
+
valid = false;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!valid) continue;
|
|
529
|
+
if (!verifyBindings(node.bindings, variables, env, program)) continue;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
throw new Error(`source proof does not validate: ${node.goal}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function verifyBindings(bindings, variables, env, program) {
|
|
536
|
+
const expected = new Map();
|
|
537
|
+
for (const [name, fresh] of variables.entries()) {
|
|
538
|
+
const resolved = deref(fresh, env);
|
|
539
|
+
if (resolved.type !== VAR) expected.set(name, resolved);
|
|
540
|
+
}
|
|
541
|
+
if (bindings.length !== expected.size) return false;
|
|
542
|
+
for (const binding of bindings) {
|
|
543
|
+
if (!binding || typeof binding.name !== 'string' || typeof binding.value !== 'string') return false;
|
|
544
|
+
const resolved = expected.get(binding.name);
|
|
545
|
+
if (!resolved) return false;
|
|
546
|
+
const value = parseCertificateTerm(binding.value, program);
|
|
547
|
+
const check = new Env();
|
|
548
|
+
if (!unify(resolveForProof(resolved, env), value, check)) return false;
|
|
549
|
+
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
253
553
|
function renderMethodTerm(method) {
|
|
254
554
|
if (method && method.type === 'source') return `${method.kind}(${quoteString(method.filename)}, clause(${method.clause}))`;
|
|
255
555
|
if (method && method.type === 'builtin') return `builtin(${quoteAtomText(method.name)}, ${method.arity})`;
|
package/src/index.js
CHANGED
|
@@ -79,7 +79,7 @@ export function run(source, options = {}) {
|
|
|
79
79
|
({ haltCode } = executeForwardRules(program, solver, {
|
|
80
80
|
onAnswer: (line, resolved) => {
|
|
81
81
|
output.push(line);
|
|
82
|
-
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
|
|
82
|
+
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry, options.proofDetail);
|
|
83
83
|
},
|
|
84
84
|
onFuse: (line) => output.push(line),
|
|
85
85
|
onDiagnostic: (line) => {
|
|
@@ -91,15 +91,15 @@ export function run(source, options = {}) {
|
|
|
91
91
|
({ haltCode } = executeGoals(program, solver, goals, {
|
|
92
92
|
onAnswer: (line, resolved) => {
|
|
93
93
|
output.push(line);
|
|
94
|
-
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
|
|
94
|
+
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry, options.proofDetail);
|
|
95
95
|
},
|
|
96
96
|
}));
|
|
97
97
|
}
|
|
98
98
|
return { stdout: output.join(''), stats: solver.stats, haltCode };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function appendExplanation(output, program, resolved, registry) {
|
|
102
|
-
const proof = whyProof(program, resolved, { registry });
|
|
101
|
+
function appendExplanation(output, program, resolved, registry, proofDetail = 'abstract') {
|
|
102
|
+
const proof = whyProof(program, resolved, { registry, proofDetail });
|
|
103
103
|
output.push(proof.text);
|
|
104
104
|
if (!proof.ok) output.push(whyNoProof(resolved));
|
|
105
105
|
}
|
package/src/write.js
CHANGED
|
@@ -152,6 +152,20 @@ function writeString(value) {
|
|
|
152
152
|
return out + '"';
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function quotedListCharacter(item, doubleQuotes) {
|
|
156
|
+
if (doubleQuotes === 'chars') {
|
|
157
|
+
if (item.type !== ATOM || Array.from(item.name).length !== 1) return null;
|
|
158
|
+
return item.name;
|
|
159
|
+
}
|
|
160
|
+
if (doubleQuotes === 'codes') {
|
|
161
|
+
if (item.type !== NUMBER || !/^\d+$/.test(item.name)) return null;
|
|
162
|
+
const code = BigInt(item.name);
|
|
163
|
+
if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
|
|
164
|
+
return String.fromCodePoint(Number(code));
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
function quotedListSplice(term, env, doubleQuotes) {
|
|
156
170
|
if (doubleQuotes !== 'chars' && doubleQuotes !== 'codes') return null;
|
|
157
171
|
const characters = [];
|
|
@@ -164,16 +178,9 @@ function quotedListSplice(term, env, doubleQuotes) {
|
|
|
164
178
|
if (!isCons(cursor)) {
|
|
165
179
|
return characters.length === 0 ? null : { text: characters.join(''), tail: cursor };
|
|
166
180
|
}
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
characters.push(item.name);
|
|
171
|
-
} else {
|
|
172
|
-
if (item.type !== NUMBER || !/^\d+$/.test(item.name)) return null;
|
|
173
|
-
const code = BigInt(item.name);
|
|
174
|
-
if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
|
|
175
|
-
characters.push(String.fromCodePoint(Number(code)));
|
|
176
|
-
}
|
|
181
|
+
const character = quotedListCharacter(deref(cursor.args[0], env), doubleQuotes);
|
|
182
|
+
if (character == null) return null;
|
|
183
|
+
characters.push(character);
|
|
177
184
|
cursor = cursor.args[1];
|
|
178
185
|
}
|
|
179
186
|
}
|
|
@@ -351,15 +358,26 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
351
358
|
|
|
352
359
|
if (!options.ignoreOps) {
|
|
353
360
|
const parts = [];
|
|
361
|
+
let quotedSuffix = [];
|
|
354
362
|
let cursor = resolved;
|
|
355
363
|
while (true) {
|
|
356
364
|
cursor = deref(cursor, env);
|
|
357
365
|
const separator = options.compact ? ',' : ', ';
|
|
358
|
-
if (isEmptyList(cursor))
|
|
366
|
+
if (isEmptyList(cursor)) {
|
|
367
|
+
if (quotedSuffix.length > 1 && quotedSuffix.length < parts.length) {
|
|
368
|
+
const prefix = parts.slice(0, parts.length - quotedSuffix.length);
|
|
369
|
+
return `[${prefix.join(separator)}|${writeString(quotedSuffix.join(''))}]`;
|
|
370
|
+
}
|
|
371
|
+
return `[${parts.join(separator)}]`;
|
|
372
|
+
}
|
|
359
373
|
if (!isCons(cursor)) {
|
|
360
374
|
const tailSeparator = options.compact ? '|' : ' | ';
|
|
361
375
|
return `[${parts.join(separator)}${tailSeparator}${format(cursor, env, options, table, 999, 'argument')}]`;
|
|
362
376
|
}
|
|
377
|
+
const item = deref(cursor.args[0], env);
|
|
378
|
+
const character = quotedListCharacter(item, options.doubleQuotes);
|
|
379
|
+
if (character == null) quotedSuffix = [];
|
|
380
|
+
else quotedSuffix.push(character);
|
|
363
381
|
parts.push(format(cursor.args[0], env, options, table, 999, 'argument'));
|
|
364
382
|
cursor = cursor.args[1];
|
|
365
383
|
}
|
|
@@ -124,7 +124,7 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
|
|
|
124
124
|
| 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and bundled-library autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
|
|
125
125
|
| 5.5.10 Evaluable functors | Normal mode additionally accepts the EyeProlog evaluable atom `e`; the remaining arithmetic functors accepted by strict mode are the Part 1 + Corrigenda set. | Strict mode rejects `e/0` as non-evaluable and retains the Corrigendum arithmetic additions. **covered** — strict extension-boundary regression plus `src/iso-arithmetic.js`. |
|
|
126
126
|
| 5.5.11 Reserved atoms | None | None. |
|
|
127
|
-
| Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(true|false)`. `double_quotes(true)` is orthogonal to `ignore_ops(true)`: eligible character/code lists retain double-quoted notation while operator terms use functional notation, and the order of those distinct options does not affect the result. `spacing(false)` emits only lexically required separators; `spacing(true)` adds conventional layout around operators. Strict core excludes these implementation-specific extensions and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
|
|
127
|
+
| Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(true|false)`. `double_quotes(true)` is orthogonal to `ignore_ops(true)`: eligible character/code lists, including a proper list's maximal character/code suffix of at least two elements, retain double-quoted notation while operator terms use functional notation, and the order of those distinct options does not affect the result. `spacing(false)` emits only lexically required separators; `spacing(true)` adds conventional layout around operators. Strict core excludes these implementation-specific extensions and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
|
|
128
128
|
|
|
129
129
|
Normal mode provides documented module and DCG compatibility profiles whose
|
|
130
130
|
features overlap standardized Part 2 and Part 3 facilities. They are extensions
|
|
@@ -1 +1 @@
|
|
|
1
|
-
univ_results(foo(a, b), [foo
|
|
1
|
+
univ_results(foo(a, b), [foo|"ab"], "a", ['.', 1, []], [1.0]).
|
package/test/run-regression.mjs
CHANGED
|
@@ -47,6 +47,9 @@ import {
|
|
|
47
47
|
unify,
|
|
48
48
|
variantTerms,
|
|
49
49
|
parseProgramText,
|
|
50
|
+
proofCertificate,
|
|
51
|
+
proofCertificatesFromText,
|
|
52
|
+
verifyProof,
|
|
50
53
|
} from '../src/index.js';
|
|
51
54
|
import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText, tryParseClausesFastInto } from '../src/parser.js';
|
|
52
55
|
import { PrologError, formalErrorTerm } from '../src/iso.js';
|
|
@@ -73,6 +76,10 @@ const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
|
73
76
|
const packageRoot = path.resolve(testRoot, '..');
|
|
74
77
|
const bin = path.join(packageRoot, 'bin', 'eyeprolog.js');
|
|
75
78
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
79
|
+
// The issue #49 hand-off checks assert demand-driven completion and bounded memory,
|
|
80
|
+
// not a machine-speed threshold. Leave enough wall-clock headroom for slower or
|
|
81
|
+
// contended CI hosts while still terminating a genuinely stuck child process.
|
|
82
|
+
const DCG_HANDOFF_TEST_TIMEOUT_MS = 20_000;
|
|
76
83
|
let tmp = null;
|
|
77
84
|
let tmpCounter = 0;
|
|
78
85
|
|
|
@@ -2863,7 +2870,7 @@ c4 ?- call((!;1)).
|
|
|
2863
2870
|
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
|
|
2864
2871
|
cwd: packageRoot,
|
|
2865
2872
|
encoding: 'utf8',
|
|
2866
|
-
timeout:
|
|
2873
|
+
timeout: DCG_HANDOFF_TEST_TIMEOUT_MS,
|
|
2867
2874
|
});
|
|
2868
2875
|
if (result.error) throw result.error;
|
|
2869
2876
|
assertEqual(result.status, 0, `65536-cell hand-off status; stderr=${result.stderr}`);
|
|
@@ -2906,7 +2913,7 @@ c4 ?- call((!;1)).
|
|
|
2906
2913
|
'length(_,E),E>12,N is 2^E,\\+ \\+ (length(L,N),time(phrase(a,L))).\n' +
|
|
2907
2914
|
';\n;\n;\n\n' +
|
|
2908
2915
|
'halt.\n',
|
|
2909
|
-
timeout:
|
|
2916
|
+
timeout: DCG_HANDOFF_TEST_TIMEOUT_MS,
|
|
2910
2917
|
});
|
|
2911
2918
|
if (result.error) throw result.error;
|
|
2912
2919
|
assertEqual(result.status, 0, `deep REPL hand-off status; stderr=${result.stderr}`);
|
|
@@ -3842,6 +3849,39 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
3842
3849
|
assertEqual(result.stderr, '', 'stderr');
|
|
3843
3850
|
},
|
|
3844
3851
|
},
|
|
3852
|
+
|
|
3853
|
+
{
|
|
3854
|
+
name: '--proof-detail expanded exposes bundled Prolog library clauses',
|
|
3855
|
+
run: () => {
|
|
3856
|
+
const input = '%% goal: q(a)\n:- use_module(library(lists)).\nq(X) :- member(X, [a,b]).\n';
|
|
3857
|
+
const result = runCli(['--proof-detail', 'expanded', '-'], { input });
|
|
3858
|
+
assertEqual(result.status, 0, 'exit status');
|
|
3859
|
+
assertIncludes(result.stdout, 'by(fact("src/lib/lists.pl"', 'expanded library source');
|
|
3860
|
+
assertNotIncludes(result.stdout, 'by(library(member, 2))', 'abstract library boundary');
|
|
3861
|
+
},
|
|
3862
|
+
},
|
|
3863
|
+
{
|
|
3864
|
+
name: '--verify-proof accepts saved why/2 proof output and rejects tampering',
|
|
3865
|
+
run: () => {
|
|
3866
|
+
const programFile = path.join(tmp, `proof-program-${++tmpCounter}.pl`);
|
|
3867
|
+
const proofFile = path.join(tmp, `proof-certificate-${++tmpCounter}.pl`);
|
|
3868
|
+
fs.writeFileSync(programFile, '%% goal: q(a)\np(a).\nq(X) :- p(X).\n');
|
|
3869
|
+
const generated = runCli(['--proof', programFile]);
|
|
3870
|
+
assertEqual(generated.status, 0, 'proof generation status');
|
|
3871
|
+
fs.writeFileSync(proofFile, generated.stdout);
|
|
3872
|
+
const verified = runCli(['--verify-proof', proofFile, programFile]);
|
|
3873
|
+
assertEqual(verified.status, 0, 'verification status');
|
|
3874
|
+
assertEqual(verified.stdout, 'verified 1 proof certificate.\n', 'verification stdout');
|
|
3875
|
+
const strictVerified = runCli(['--iso-strict', '--verify-proof', proofFile, programFile]);
|
|
3876
|
+
assertEqual(strictVerified.status, 0, 'strict verification status');
|
|
3877
|
+
assertEqual(strictVerified.stdout, 'verified 1 proof certificate.\n', 'strict verification stdout');
|
|
3878
|
+
const tamperedFile = path.join(tmp, `proof-certificate-bad-${++tmpCounter}.pl`);
|
|
3879
|
+
fs.writeFileSync(tamperedFile, generated.stdout.replace('goal(p(a))', 'goal(p(b))'));
|
|
3880
|
+
const rejected = runCli(['--verify-proof', tamperedFile, programFile]);
|
|
3881
|
+
assertEqual(rejected.status, 1, 'tampered verification status');
|
|
3882
|
+
assertIncludes(rejected.stderr, 'proof certificate 1 failed verification', 'tampered verification stderr');
|
|
3883
|
+
},
|
|
3884
|
+
},
|
|
3845
3885
|
{
|
|
3846
3886
|
name: '-pw combines proof and warning flags',
|
|
3847
3887
|
run: () => {
|
|
@@ -5624,6 +5664,74 @@ true :+ ready.
|
|
|
5624
5664
|
},
|
|
5625
5665
|
},
|
|
5626
5666
|
|
|
5667
|
+
{
|
|
5668
|
+
name: 'proof certificates verify source derivations without proof search',
|
|
5669
|
+
run: () => {
|
|
5670
|
+
const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
|
|
5671
|
+
const result = proofCertificate(program, parseGoalText('q(a, b)'));
|
|
5672
|
+
assertEqual(result.ok, true, 'certificate generated');
|
|
5673
|
+
assertEqual(result.certificate.version, 1, 'certificate version');
|
|
5674
|
+
assertEqual(result.certificate.answer, 'q(a, b)', 'certificate answer');
|
|
5675
|
+
const checked = verifyProof(program, result);
|
|
5676
|
+
assertEqual(checked.ok, true, 'certificate verifies');
|
|
5677
|
+
assertEqual(checked.trusted.length, 0, 'source-only certificate has no trusted boundaries');
|
|
5678
|
+
},
|
|
5679
|
+
},
|
|
5680
|
+
{
|
|
5681
|
+
name: 'proof verification rejects a tampered child goal',
|
|
5682
|
+
run: () => {
|
|
5683
|
+
const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
|
|
5684
|
+
const result = proofCertificate(program, parseGoalText('q(a, b)'));
|
|
5685
|
+
const tampered = structuredClone(result.certificate);
|
|
5686
|
+
tampered.proof.children[0].goal = 'p(a, c)';
|
|
5687
|
+
assertEqual(verifyProof(program, tampered).ok, false, 'tampered child is rejected');
|
|
5688
|
+
const bindingTamper = structuredClone(result.certificate);
|
|
5689
|
+
bindingTamper.proof.bindings[1].value = 'c';
|
|
5690
|
+
assertEqual(verifyProof(program, bindingTamper).ok, false, 'tampered binding is rejected');
|
|
5691
|
+
const changedProgram = Program.parse('p(a, c).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
|
|
5692
|
+
assertEqual(verifyProof(changedProgram, result).ok, false, 'changed source program is rejected');
|
|
5693
|
+
},
|
|
5694
|
+
},
|
|
5695
|
+
{
|
|
5696
|
+
name: 'why/2 text round-trips into a verifiable proof certificate',
|
|
5697
|
+
run: () => {
|
|
5698
|
+
const program = Program.parse('p(a).\nq(X) :- p(X).\n', { sourceMetadata: true });
|
|
5699
|
+
const result = proofCertificate(program, parseGoalText('q(a)'));
|
|
5700
|
+
const parsed = proofCertificatesFromText(result.text, program);
|
|
5701
|
+
assertEqual(parsed.length, 1, 'certificate count');
|
|
5702
|
+
assertEqual(verifyProof(program, parsed[0]).ok, true, 'round-tripped certificate verifies');
|
|
5703
|
+
},
|
|
5704
|
+
},
|
|
5705
|
+
{
|
|
5706
|
+
name: 'proof certificate text round-trips under every double_quotes mode',
|
|
5707
|
+
run: () => {
|
|
5708
|
+
for (const doubleQuotes of ['chars', 'codes', 'atom']) {
|
|
5709
|
+
const program = Program.parse('p(a).\nq(X) :- p(X).\n', { sourceMetadata: true, doubleQuotes });
|
|
5710
|
+
const result = proofCertificate(program, parseGoalText('q(a)', { doubleQuotes }));
|
|
5711
|
+
const parsed = proofCertificatesFromText(result.text, program);
|
|
5712
|
+
assertEqual(parsed.length, 1, `${doubleQuotes} certificate count`);
|
|
5713
|
+
assertEqual(verifyProof(program, parsed[0]).ok, true, `${doubleQuotes} certificate verifies`);
|
|
5714
|
+
}
|
|
5715
|
+
},
|
|
5716
|
+
},
|
|
5717
|
+
{
|
|
5718
|
+
name: 'expanded proof detail opens bundled Prolog library clauses',
|
|
5719
|
+
run: () => {
|
|
5720
|
+
const program = Program.parse(':- use_module(library(lists)).\nq(X) :- member(X, [a,b]).\n', { sourceMetadata: true });
|
|
5721
|
+
const abstract = proofCertificate(program, parseGoalText('q(a)'));
|
|
5722
|
+
const expanded = proofCertificate(program, parseGoalText('q(a)'), { proofDetail: 'expanded' });
|
|
5723
|
+
assertEqual(abstract.certificate.proof.children[0].method.type, 'library', 'abstract library boundary');
|
|
5724
|
+
assertEqual(expanded.certificate.proof.children[0].method.type, 'source', 'expanded library source');
|
|
5725
|
+
const abstractChecked = verifyProof(program, abstract);
|
|
5726
|
+
const expandedChecked = verifyProof(program, expanded);
|
|
5727
|
+
assertEqual(abstractChecked.ok, true, 'abstract certificate verifies');
|
|
5728
|
+
assertEqual(abstractChecked.trusted.length, 1, 'abstract certificate reports library trust');
|
|
5729
|
+
assertEqual(abstractChecked.trusted[0].type, 'library', 'reported trust boundary type');
|
|
5730
|
+
assertEqual(expandedChecked.ok, true, 'expanded certificate verifies');
|
|
5731
|
+
assertEqual(expandedChecked.trusted.length, 0, 'pure-Prolog expanded certificate removes library trust');
|
|
5732
|
+
},
|
|
5733
|
+
},
|
|
5734
|
+
|
|
5627
5735
|
{
|
|
5628
5736
|
name: 'run accepts Program instances',
|
|
5629
5737
|
run: () => {
|
|
@@ -7171,6 +7279,92 @@ function whiteBoxCases() {
|
|
|
7171
7279
|
'double_quotes(codes) representation');
|
|
7172
7280
|
},
|
|
7173
7281
|
},
|
|
7282
|
+
{
|
|
7283
|
+
name: 'double-quoted character and code suffixes are recognized inside proper lists (issue #88 suffix follow-up)',
|
|
7284
|
+
run: () => {
|
|
7285
|
+
const repl = runCli([], {
|
|
7286
|
+
input: 'T = [A,b,c,d,e,f].\nhalt.\n',
|
|
7287
|
+
});
|
|
7288
|
+
assertEqual(repl.status, 0, 'REPL exit status');
|
|
7289
|
+
assertIncludes(repl.stdout, 'T = [A|"bcdef"].', 'Scryer-compatible character suffix');
|
|
7290
|
+
assertNotIncludes(repl.stdout, 'T = [A, b, c, d, e, f].', 'expanded suffix suppressed');
|
|
7291
|
+
|
|
7292
|
+
const chars = parseGoalText('p([foo,b,c])').args[0];
|
|
7293
|
+
assertEqual(
|
|
7294
|
+
formatTermForWrite(chars, new Env(), {
|
|
7295
|
+
quoted: true,
|
|
7296
|
+
doubleQuotes: 'chars',
|
|
7297
|
+
doubleBar: true,
|
|
7298
|
+
compact: true,
|
|
7299
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7300
|
+
}),
|
|
7301
|
+
'[foo|"bc"]',
|
|
7302
|
+
'ordinary list notation uses a quoted suffix',
|
|
7303
|
+
);
|
|
7304
|
+
assertEqual(
|
|
7305
|
+
formatTermForWrite(chars, new Env(), {
|
|
7306
|
+
quoted: true,
|
|
7307
|
+
ignoreOps: true,
|
|
7308
|
+
doubleQuotes: 'chars',
|
|
7309
|
+
doubleBar: true,
|
|
7310
|
+
compact: true,
|
|
7311
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7312
|
+
}),
|
|
7313
|
+
`'.'(foo,"bc")`,
|
|
7314
|
+
'ignore_ops remains orthogonal to suffix quoting',
|
|
7315
|
+
);
|
|
7316
|
+
assertEqual(
|
|
7317
|
+
formatTermForWrite(chars, new Env(), {
|
|
7318
|
+
quoted: true,
|
|
7319
|
+
doubleQuotes: null,
|
|
7320
|
+
doubleBar: true,
|
|
7321
|
+
compact: true,
|
|
7322
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7323
|
+
}),
|
|
7324
|
+
'[foo,b,c]',
|
|
7325
|
+
'disabled double_quotes keeps expanded list notation',
|
|
7326
|
+
);
|
|
7327
|
+
const improper = parseGoalText('p([foo,b,c|tail])').args[0];
|
|
7328
|
+
assertEqual(
|
|
7329
|
+
formatTermForWrite(improper, new Env(), {
|
|
7330
|
+
quoted: true,
|
|
7331
|
+
doubleQuotes: 'chars',
|
|
7332
|
+
doubleBar: true,
|
|
7333
|
+
compact: true,
|
|
7334
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7335
|
+
}),
|
|
7336
|
+
'[foo,b,c|tail]',
|
|
7337
|
+
'improper suffix is not mistaken for a proper string suffix',
|
|
7338
|
+
);
|
|
7339
|
+
|
|
7340
|
+
const astral = parseGoalText("p([foo,'😀',x])").args[0];
|
|
7341
|
+
assertEqual(
|
|
7342
|
+
formatTermForWrite(astral, new Env(), {
|
|
7343
|
+
quoted: true,
|
|
7344
|
+
doubleQuotes: 'chars',
|
|
7345
|
+
doubleBar: true,
|
|
7346
|
+
compact: true,
|
|
7347
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7348
|
+
}),
|
|
7349
|
+
'[foo|"😀x"]',
|
|
7350
|
+
'Unicode scalar suffix',
|
|
7351
|
+
);
|
|
7352
|
+
|
|
7353
|
+
const codes = parseGoalText('p([foo,98,128512])', { doubleQuotes: 'codes' }).args[0];
|
|
7354
|
+
assertEqual(
|
|
7355
|
+
formatTermForWrite(codes, new Env(), {
|
|
7356
|
+
quoted: true,
|
|
7357
|
+
doubleQuotes: 'codes',
|
|
7358
|
+
doubleBar: true,
|
|
7359
|
+
compact: true,
|
|
7360
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7361
|
+
}),
|
|
7362
|
+
'[foo|"b😀"]',
|
|
7363
|
+
'code-list suffix',
|
|
7364
|
+
);
|
|
7365
|
+
},
|
|
7366
|
+
},
|
|
7367
|
+
|
|
7174
7368
|
{
|
|
7175
7369
|
name: 'charsio write_term_to_chars composes double_quotes with ignore_ops',
|
|
7176
7370
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1467,25 +1467,42 @@ why(
|
|
|
1467
1467
|
).
|
|
1468
1468
|
```
|
|
1469
1469
|
|
|
1470
|
-
Proof output is valid EyeProlog input:
|
|
1470
|
+
Proof output is valid EyeProlog input and can be kept as a proof certificate:
|
|
1471
1471
|
|
|
1472
1472
|
```sh
|
|
1473
1473
|
eyeprolog --proof examples/socrates.pl > socrates.why.pl
|
|
1474
|
+
eyeprolog --verify-proof socrates.why.pl examples/socrates.pl
|
|
1474
1475
|
```
|
|
1475
1476
|
|
|
1477
|
+
The second command checks the supplied `why/2` derivation against the program;
|
|
1478
|
+
it does not search again for a proof. A changed source clause, child goal, source
|
|
1479
|
+
location, or recorded substitution makes the certificate fail verification.
|
|
1480
|
+
Certificate input is parsed as Prolog data rather than loaded as a program, so
|
|
1481
|
+
its terms cannot trigger directives while being checked.
|
|
1482
|
+
|
|
1476
1483
|
A normal answer is one resolved ground term followed by a period. Strings,
|
|
1477
1484
|
quoted atoms, lists, and compounds are rendered in supported source syntax so
|
|
1478
1485
|
the output can be read back. Enabling `--proof`, `--warnings`, or `--stats`
|
|
1479
1486
|
must not change which answers are found.
|
|
1480
1487
|
|
|
1481
|
-
The second argument of `why/2` is
|
|
1488
|
+
The second argument of `why/2` is a proof term of the general shape
|
|
1482
1489
|
`proof(goal(G), by(Method), bindings(Bindings), uses(Proofs))`. User clauses
|
|
1483
1490
|
are identified as `fact(Filename, clause(N))` or
|
|
1484
1491
|
`rule(Filename, clause(N))`, with one-based source clause numbers. Built-ins
|
|
1485
|
-
are identified as `builtin(Name, Arity)`.
|
|
1492
|
+
are identified as `builtin(Name, Arity)`. By default, bundled Prolog-library
|
|
1493
|
+
predicates appear as `library(Name, Arity)` trusted boundaries. Use
|
|
1494
|
+
`--proof-detail expanded` to replace those boundaries with the library source
|
|
1495
|
+
clauses and any trusted built-ins they call. Explanation data is outside the
|
|
1486
1496
|
logical semantics of the input program: it describes the derivation but does
|
|
1487
1497
|
not participate in finding it.
|
|
1488
1498
|
|
|
1499
|
+
Verification checks source steps structurally: the named clause must exist, its
|
|
1500
|
+
head must unify with the certified goal, its body must correspond to the child
|
|
1501
|
+
proofs, and recorded clause-variable bindings must agree with that derivation.
|
|
1502
|
+
Built-ins and abstract library nodes are deliberately trusted boundaries. This
|
|
1503
|
+
separates proof discovery from proof checking without pretending that host
|
|
1504
|
+
operations can be justified by Prolog source that does not exist.
|
|
1505
|
+
|
|
1489
1506
|
A second program can query `why/2`. Read a proof as an argument. If it contains
|
|
1490
1507
|
irrelevant detours, improve the helpers. If a key premise is hidden inside an
|
|
1491
1508
|
opaque value, model it as a fact. Designing for a good explanation often
|
|
@@ -1862,10 +1879,42 @@ prints numeric work counters; those counters describe this run rather than an
|
|
|
1862
1879
|
additional logical answer.
|
|
1863
1880
|
|
|
1864
1881
|
`run/2` accepts source text or an already parsed `Program`. Its options include
|
|
1865
|
-
`proof` (with `why` and `explain` as aliases), `
|
|
1866
|
-
`
|
|
1867
|
-
|
|
1868
|
-
nullable `haltCode`; it does not
|
|
1882
|
+
`proof` (with `why` and `explain` as aliases), `proofDetail` (`abstract` or
|
|
1883
|
+
`expanded`), `maxDepth`, `maxInferences`, `maxMemoryBytes`, `solutionLimit`, a
|
|
1884
|
+
custom `registry`, and `strictNegation` or `analyzeNegation`. It returns
|
|
1885
|
+
`stdout`, the solver's numeric `stats`, and a nullable `haltCode`; it does not
|
|
1886
|
+
write to the process streams.
|
|
1887
|
+
|
|
1888
|
+
For applications that exchange proofs independently of answer output, the same
|
|
1889
|
+
module exposes `proofCertificate(program, goal, options)`,
|
|
1890
|
+
`proofCertificatesFromText(text, program)`, and
|
|
1891
|
+
`verifyProof(program, certificate, options)`:
|
|
1892
|
+
|
|
1893
|
+
```js
|
|
1894
|
+
import {
|
|
1895
|
+
Program, parseGoalText,
|
|
1896
|
+
proofCertificate, proofCertificatesFromText, verifyProof
|
|
1897
|
+
} from 'eyeprolog';
|
|
1898
|
+
|
|
1899
|
+
const program = Program.parse(`
|
|
1900
|
+
p(a).
|
|
1901
|
+
q(X) :- p(X).
|
|
1902
|
+
`, { sourceMetadata: true });
|
|
1903
|
+
|
|
1904
|
+
const made = proofCertificate(program, parseGoalText('q(a)'));
|
|
1905
|
+
console.log(verifyProof(program, made).ok); // true
|
|
1906
|
+
|
|
1907
|
+
const received = proofCertificatesFromText(made.text, program)[0];
|
|
1908
|
+
console.log(verifyProof(program, received).ok); // true
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
`proofCertificate` returns both the ordinary `why/2` text and a JSON-serializable
|
|
1912
|
+
certificate object. `verifyProof` walks the supplied certificate rather than
|
|
1913
|
+
asking the solver to find another proof; its `trusted` array lists every builtin
|
|
1914
|
+
or abstract-library boundary that was assumed while checking it. Passing
|
|
1915
|
+
`proofDetail: 'expanded'` exposes bundled Prolog-library clauses in the
|
|
1916
|
+
certificate and therefore reduces library-level trust boundaries to the
|
|
1917
|
+
built-ins those clauses ultimately use.
|
|
1869
1918
|
|
|
1870
1919
|
When `run` receives an already parsed `Program`, bundled-library imports
|
|
1871
1920
|
needed only by its host-supplied goals are added to that Program before solving,
|
|
@@ -5480,9 +5529,11 @@ still retained where adjacent graphic tokens would otherwise merge, as in
|
|
|
5480
5529
|
`write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
|
|
5481
5530
|
`variable_names/1`. Normal mode additionally accepts the EyeProlog extension
|
|
5482
5531
|
`double_quotes(true|false)`: `true` lets eligible character/code lists use the
|
|
5483
|
-
current `double_quotes` representation. Proper lists can therefore be written
|
|
5484
|
-
as `"text"
|
|
5485
|
-
|
|
5532
|
+
current `double_quotes` representation. Proper character/code lists can therefore be written
|
|
5533
|
+
as `"text"`. A proper list whose final segment contains at least two characters/codes
|
|
5534
|
+
uses that representation for the suffix, so `[A,b,c,d,e,f]` is written as
|
|
5535
|
+
`[A|"bcdef"]`. A partial list such as `[a,b|Tail]` is written as `"ab"||Tail`.
|
|
5536
|
+
This representation choice is independent of `ignore_ops/1`: with
|
|
5486
5537
|
`ignore_ops(true)`, operator terms use functional notation while an explicitly
|
|
5487
5538
|
requested character/code list remains double quoted. Thus
|
|
5488
5539
|
`write_term(f("ab",a+b),[quoted(true),ignore_ops(true),double_quotes(true)])`
|
|
@@ -5547,6 +5598,21 @@ and document the command line; the conformance corpus fixes success, failure,
|
|
|
5547
5598
|
mode, and error behavior. Use this part for working practice and those chapters
|
|
5548
5599
|
for exact reference.
|
|
5549
5600
|
|
|
5601
|
+
### Historical note: the practical language becomes portable
|
|
5602
|
+
|
|
5603
|
+
By the late 1970s and 1980s, Prolog had spread from its Marseille origins into
|
|
5604
|
+
several implementation traditions. The Edinburgh and DECsystem-10 lineage in
|
|
5605
|
+
particular helped establish a practical programming vocabulary around ordered
|
|
5606
|
+
control, term inspection, dynamic clauses, operators, and streams. These
|
|
5607
|
+
facilities made Prolog useful far beyond theorem-proving examples, but
|
|
5608
|
+
differences between systems also made portability a recurring concern.
|
|
5609
|
+
|
|
5610
|
+
ISO/IEC 13211-1:1995 gave that accumulated practice a common core. It did not
|
|
5611
|
+
freeze the language: constraints, modules, tabling, coroutining, and other
|
|
5612
|
+
facilities continued to develop in implementations and later standardization
|
|
5613
|
+
work. What the standard supplied was a stable shared account of ordinary Prolog
|
|
5614
|
+
terms, control, state, errors, and I/O against which extensions could be named.
|
|
5615
|
+
|
|
5550
5616
|
# Part IX — Reference as practice
|
|
5551
5617
|
|
|
5552
5618
|
Reference is useful only when the route into it is clear. Begin with the task
|
|
@@ -9319,6 +9385,8 @@ make the observed question explicit.
|
|
|
9319
9385
|
| --- | --- |
|
|
9320
9386
|
| `-h`, `--help` | Show usage |
|
|
9321
9387
|
| `-p`, `--proof` | Print `why/2` explanations |
|
|
9388
|
+
| `--proof-detail abstract|expanded` | Select library abstraction for proof output; implies `--proof` |
|
|
9389
|
+
| `--verify-proof File` | Verify saved `why/2` proof certificates against the input program without proof search |
|
|
9322
9390
|
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
9323
9391
|
| `--iso-strict` | Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions (including `table` and `:+`) and disable bundled-library autoloading |
|
|
9324
9392
|
| `--portable` | Enforce the conservative EyeProlog/Trealla/Scryer interoperability profile |
|
|
@@ -9363,7 +9431,9 @@ Work in a fixed sequence:
|
|
|
9363
9431
|
|
|
9364
9432
|
1. predict the ground answers before running the program;
|
|
9365
9433
|
2. run without observation flags and compare stdout with that prediction;
|
|
9366
|
-
3. add `--proof` when the support for an answer is the question;
|
|
9434
|
+
3. add `--proof` when the support for an answer is the question; save the
|
|
9435
|
+
output and use `--verify-proof` when the derivation itself must cross a
|
|
9436
|
+
process or review boundary;
|
|
9367
9437
|
4. add `--warnings` when portability or negative dependencies are the
|
|
9368
9438
|
question; use `--portable` when non-profile dependencies must fail CI;
|
|
9369
9439
|
5. add `--stats` only when comparing two executions of the same semantic case.
|
|
@@ -9373,6 +9443,8 @@ For example:
|
|
|
9373
9443
|
```sh
|
|
9374
9444
|
eyeprolog --goal 'ancestor(X, Y)' examples/ancestor.pl
|
|
9375
9445
|
eyeprolog --proof --goal 'type(X, Y)' examples/socrates.pl
|
|
9446
|
+
eyeprolog --proof examples/socrates.pl > socrates.why.pl
|
|
9447
|
+
eyeprolog --verify-proof socrates.why.pl examples/socrates.pl
|
|
9376
9448
|
eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
|
|
9377
9449
|
eyeprolog --portable --goal 'sudoku9_solution(S)' examples/clpz-sudoku-9x9.pl
|
|
9378
9450
|
eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
|
|
@@ -10594,6 +10666,22 @@ variables. Variant recognition is important for tabling and cycle analysis.
|
|
|
10594
10666
|
**Witness.** A constructed ground term demonstrating an existential result,
|
|
10595
10667
|
such as a path, assignment, factorization, schedule, or proof-relevant object.
|
|
10596
10668
|
|
|
10669
|
+
### Historical note: manuals become specifications
|
|
10670
|
+
|
|
10671
|
+
Early Prolog programmers learned from implementation manuals, examples, and
|
|
10672
|
+
books whose descriptions were often inseparable from one particular system. As
|
|
10673
|
+
the language spread, reference writing acquired a second task: distinguish the
|
|
10674
|
+
portable language from implementation convention. Predicate indexes, precise
|
|
10675
|
+
mode and error descriptions, and standards documents became tools for comparing
|
|
10676
|
+
systems rather than merely operating one of them.
|
|
10677
|
+
|
|
10678
|
+
ISO standardization made that distinction explicit, while conformance tests and
|
|
10679
|
+
cross-processor corpora made many disagreements executable. A mature reference
|
|
10680
|
+
therefore joins several forms of evidence: normative prose, named predicates
|
|
10681
|
+
and flags, examples, implementation boundaries, and reproducible tests. The
|
|
10682
|
+
result is not a substitute for programming practice; it is a map from a
|
|
10683
|
+
concrete question to the exact contract that governs it.
|
|
10684
|
+
|
|
10597
10685
|
# Part X — Laboratories
|
|
10598
10686
|
|
|
10599
10687
|
## 44. Twelve laboratories
|
|
@@ -10858,6 +10946,21 @@ A beautiful program is not merely short. It makes the reason for its
|
|
|
10858
10946
|
correctness, the shape of its search, and the boundary of its trust available
|
|
10859
10947
|
to the next reader.
|
|
10860
10948
|
|
|
10949
|
+
### Historical note: logic programming grows through exercises
|
|
10950
|
+
|
|
10951
|
+
Logic programming has long been taught by construction. Lists, family
|
|
10952
|
+
relations, puzzles, grammars, interpreters, search problems, and small expert
|
|
10953
|
+
systems became recurring exercises because each exposes both a logical relation
|
|
10954
|
+
and the control needed to compute with it. Texts such as *The Art of Prolog*
|
|
10955
|
+
made this dual reading central: an exercise was not finished when a clause
|
|
10956
|
+
parsed, but when its meaning, modes, and behavior could be explained.
|
|
10957
|
+
|
|
10958
|
+
Laboratory practice later absorbed regression testing, property-oriented
|
|
10959
|
+
checking, benchmark corpora, and reproducible command-line runs. These tools
|
|
10960
|
+
fit logic programming unusually well because a small change can be examined at
|
|
10961
|
+
several levels at once: answers, failures, witnesses, proofs, and search. The
|
|
10962
|
+
laboratory is where a declarative claim becomes an executable experiment.
|
|
10963
|
+
|
|
10861
10964
|
# Part XI — Review
|
|
10862
10965
|
|
|
10863
10966
|
## 45. Checkpoint notes and selected answers
|
|
@@ -10982,3 +11085,17 @@ unbounded theorem.
|
|
|
10982
11085
|
|
|
10983
11086
|
For laboratory checkpoints, leave an artifact. A useful completion is not merely a paragraph: it is a small source file, predicted output, actual output, and one sentence explaining any difference.
|
|
10984
11087
|
|
|
11088
|
+
### Historical note: review becomes explanation
|
|
11089
|
+
|
|
11090
|
+
The declarative reading of logic programs encouraged debugging methods that ask
|
|
11091
|
+
what a relation was intended to mean, not only which machine step came next. In
|
|
11092
|
+
the early 1980s, Ehud Shapiro's work on algorithmic debugging used computation
|
|
11093
|
+
trees together with a programmer's judgments about intended results to narrow a
|
|
11094
|
+
fault to the clause responsible for it. Declarative debugging developed this
|
|
11095
|
+
idea alongside, rather than instead of, ordinary tracing.
|
|
11096
|
+
|
|
11097
|
+
That tradition gives review a distinctive role in logic programming. A worked
|
|
11098
|
+
answer is useful when it can be reconstructed from the relation, the calling
|
|
11099
|
+
mode, and the evidence, and when a disagreement can be turned into a smaller
|
|
11100
|
+
question about meaning or control. Review then becomes another pass through the
|
|
11101
|
+
same discipline as programming: predict, execute, explain, and revise.
|
package/why-eyeprolog.md
CHANGED
|
@@ -203,9 +203,20 @@ make decisions, combine data from several sources, or need to be reviewed by
|
|
|
203
203
|
someone who did not write them.
|
|
204
204
|
|
|
205
205
|
Proofs make successful reasoning easier to inspect, test, teach, and discuss.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
EyeProlog also treats a successful `why/2` term as a portable proof certificate.
|
|
207
|
+
A certificate can be saved, transmitted as ordinary Prolog data, and checked later
|
|
208
|
+
against the program without repeating the search that found the answer. Source
|
|
209
|
+
steps are checked against the named clauses and their substitutions. Built-ins
|
|
210
|
+
and abstract library steps remain explicit trusted boundaries; expanded proof
|
|
211
|
+
detail opens bundled Prolog-library clauses so that more of the derivation can
|
|
212
|
+
be checked from source. The verification result enumerates the remaining trusted
|
|
213
|
+
boundaries rather than folding them into an undifferentiated success result.
|
|
214
|
+
|
|
215
|
+
Verification and discovery therefore have different jobs: solving searches for
|
|
216
|
+
a derivation, while certificate verification checks a supplied derivation. The
|
|
217
|
+
certificate does not authenticate source data or replace application security.
|
|
218
|
+
Embedders remain responsible for validating inputs and imposing suitable time,
|
|
219
|
+
memory, depth, and solution limits.
|
|
209
220
|
|
|
210
221
|
## One engine across JavaScript environments
|
|
211
222
|
|