eyeprolog 1.6.6 → 1.6.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/examples/proof/clpb-boolean-circuit.pl +3 -0
- package/examples/proof/clpb-cardinality.pl +40 -0
- package/examples/proof/clpb-feature-model.pl +8 -0
- package/package.json +1 -1
- package/src/cli.js +35 -22
- package/src/explain.js +24 -3
- package/src/index.js +21 -4
- package/test/regression/cases-regression.mjs +4 -4
- package/test/run-proof-checking.mjs +27 -9
- package/the-art-of-eyeprolog.md +4 -4
|
@@ -10,6 +10,7 @@ clause(1,
|
|
|
10
10
|
clause(2,
|
|
11
11
|
xor_row(row(var('X'), var('Y'), var('Z'))),
|
|
12
12
|
(xor_circuit(var('X'), var('Y'), var('Z')), labeling([var('X'), var('Y'), var('Z')]))).
|
|
13
|
+
clause(3, xor_circuit_verified(var('T')), taut(x # y =:= x * ~ y + ~ x * y, var('T'))).
|
|
13
14
|
|
|
14
15
|
step(xor_row(row(0, 0, 0)),
|
|
15
16
|
rule(2),
|
|
@@ -39,3 +40,5 @@ step(xor_row(row(1, 0, 1)),
|
|
|
39
40
|
step(xor_circuit(1, 0, 1), rule(1), ['X' = 1, 'Y' = 0, 'Z' = 1], [sat(1 =:= 1 * ~ 0 + ~ 1 * 0)]).
|
|
40
41
|
step(sat(1 =:= 1 * ~ 0 + ~ 1 * 0), builtin, [], []).
|
|
41
42
|
step(labeling([1, 0, 1]), builtin, [], []).
|
|
43
|
+
step(xor_circuit_verified(1), rule(3), ['T' = 1], [taut(x # y =:= x * ~ y + ~ x * y, 1)]).
|
|
44
|
+
step(taut(x # y =:= x * ~ y + ~ x * y, 1), builtin, [], []).
|
|
@@ -1,3 +1,43 @@
|
|
|
1
1
|
review_quorum(selection(alice(0), bob(0), carol(1), dan(1))).
|
|
2
2
|
review_quorum(selection(alice(0), bob(1), carol(1), dan(0))).
|
|
3
3
|
review_quorum_count(2).
|
|
4
|
+
|
|
5
|
+
clause(1,
|
|
6
|
+
review_constraints(var('Alice'), var('Bob'), var('Carol'), var('Dan')),
|
|
7
|
+
sat(card([2], [var('Alice'), var('Bob'), var('Carol'), var('Dan')]) * (var('Alice') =< var('Carol')) * (var('Bob') # var('Dan')))).
|
|
8
|
+
clause(2,
|
|
9
|
+
review_quorum(selection(alice(var('Alice')), bob(var('Bob')), carol(var('Carol')), dan(var('Dan')))),
|
|
10
|
+
(review_constraints(var('Alice'), var('Bob'), var('Carol'), var('Dan')),
|
|
11
|
+
labeling([var('Alice'), var('Bob'), var('Carol'), var('Dan')]))).
|
|
12
|
+
clause(3,
|
|
13
|
+
review_quorum_count(var('Count')),
|
|
14
|
+
sat_count(card([2], [var('Alice'), var('Bob'), var('Carol'), var('Dan')]) * (var('Alice') =< var('Carol')) * (var('Bob') # var('Dan')), var('Count'))).
|
|
15
|
+
|
|
16
|
+
step(review_quorum(selection(alice(0), bob(0), carol(1), dan(1))),
|
|
17
|
+
rule(2),
|
|
18
|
+
['Alice' = 0, 'Bob' = 0, 'Carol' = 1, 'Dan' = 1],
|
|
19
|
+
[review_constraints(0, 0, 1, 1), labeling([0, 0, 1, 1])]).
|
|
20
|
+
step(review_constraints(0, 0, 1, 1),
|
|
21
|
+
rule(1),
|
|
22
|
+
['Alice' = 0, 'Bob' = 0, 'Carol' = 1, 'Dan' = 1],
|
|
23
|
+
[sat(card([2], [0, 0, 1, 1]) * (0 =< 1) * (0 # 1))]).
|
|
24
|
+
step(sat(card([2], [0, 0, 1, 1]) * (0 =< 1) * (0 # 1)), builtin, [], []).
|
|
25
|
+
step(labeling([0, 0, 1, 1]), builtin, [], []).
|
|
26
|
+
step(review_quorum(selection(alice(0), bob(1), carol(1), dan(0))),
|
|
27
|
+
rule(2),
|
|
28
|
+
['Alice' = 0, 'Bob' = 1, 'Carol' = 1, 'Dan' = 0],
|
|
29
|
+
[review_constraints(0, 1, 1, 0), labeling([0, 1, 1, 0])]).
|
|
30
|
+
step(review_constraints(0, 1, 1, 0),
|
|
31
|
+
rule(1),
|
|
32
|
+
['Alice' = 0, 'Bob' = 1, 'Carol' = 1, 'Dan' = 0],
|
|
33
|
+
[sat(card([2], [0, 1, 1, 0]) * (0 =< 1) * (1 # 0))]).
|
|
34
|
+
step(sat(card([2], [0, 1, 1, 0]) * (0 =< 1) * (1 # 0)), builtin, [], []).
|
|
35
|
+
step(labeling([0, 1, 1, 0]), builtin, [], []).
|
|
36
|
+
step(review_quorum_count(2),
|
|
37
|
+
rule(3),
|
|
38
|
+
['Count' = 2],
|
|
39
|
+
[sat_count(card([2], [Alice, Bob, Carol, Dan]) * (Alice =< Carol) * (Bob # Dan), 2)]).
|
|
40
|
+
step(sat_count(card([2], [Alice, Bob, Carol, Dan]) * (Alice =< Carol) * (Bob # Dan), 2),
|
|
41
|
+
builtin,
|
|
42
|
+
[],
|
|
43
|
+
[]).
|
|
@@ -11,6 +11,9 @@ clause(2,
|
|
|
11
11
|
feature_plan(features(cloud(var('Cloud')), edge(var('Edge')), audit(var('Audit')), encryption(var('Encryption')))),
|
|
12
12
|
(feature_constraints(var('Cloud'), var('Edge'), var('Audit'), var('Encryption')),
|
|
13
13
|
labeling([var('Cloud'), var('Edge'), var('Audit'), var('Encryption')]))).
|
|
14
|
+
clause(3,
|
|
15
|
+
feature_plan_count(var('Count')),
|
|
16
|
+
sat_count((anonymous(1) # var('Edge')) * (var('Audit') =< anonymous(2)) * (var('Edge') =< var('Audit')), var('Count'))).
|
|
14
17
|
|
|
15
18
|
step(feature_plan(features(cloud(0), edge(1), audit(1), encryption(1))),
|
|
16
19
|
rule(2),
|
|
@@ -52,3 +55,8 @@ step(feature_constraints(1, 0, 1, 1),
|
|
|
52
55
|
[sat((1 # 0) * (1 =< 1) * (0 =< 1))]).
|
|
53
56
|
step(sat((1 # 0) * (1 =< 1) * (0 =< 1)), builtin, [], []).
|
|
54
57
|
step(labeling([1, 0, 1, 1]), builtin, [], []).
|
|
58
|
+
step(feature_plan_count(4),
|
|
59
|
+
rule(3),
|
|
60
|
+
['Count' = 4],
|
|
61
|
+
[sat_count((_Cloud # Edge) * (Audit =< _Encryption) * (Edge =< Audit), 4)]).
|
|
62
|
+
step(sat_count((_Cloud # Edge) * (Audit =< _Encryption) * (Edge =< Audit), 4), builtin, [], []).
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function main(argv) {
|
|
|
35
35
|
files: [],
|
|
36
36
|
proof: false,
|
|
37
37
|
proofDetail: 'abstract',
|
|
38
|
-
|
|
38
|
+
checkProof: null,
|
|
39
39
|
quads: false,
|
|
40
40
|
quiet: false,
|
|
41
41
|
stats: false,
|
|
@@ -64,10 +64,10 @@ export async function main(argv) {
|
|
|
64
64
|
if (detail !== 'abstract' && detail !== 'expanded') throw new Error('--proof-detail requires abstract or expanded');
|
|
65
65
|
options.proof = true;
|
|
66
66
|
options.proofDetail = detail;
|
|
67
|
-
} else if (!endOptions && arg === '--
|
|
67
|
+
} else if (!endOptions && arg === '--check-proof') {
|
|
68
68
|
const file = argv[++i];
|
|
69
|
-
if (file == null) throw new Error('--
|
|
70
|
-
options.
|
|
69
|
+
if (file == null) throw new Error('--check-proof requires a file');
|
|
70
|
+
options.checkProof = file;
|
|
71
71
|
} else if (!endOptions && (arg === '--quads' || arg === '-q')) {
|
|
72
72
|
options.quads = true;
|
|
73
73
|
} else if (!endOptions && arg === '--quiet') {
|
|
@@ -117,18 +117,18 @@ export async function main(argv) {
|
|
|
117
117
|
if (options.isoStrict && options.quads) {
|
|
118
118
|
throw new Error('--iso-strict cannot be combined with --quads');
|
|
119
119
|
}
|
|
120
|
-
if (options.
|
|
121
|
-
throw new Error('--
|
|
120
|
+
if (options.checkProof != null && options.quads) {
|
|
121
|
+
throw new Error('--check-proof cannot be combined with --quads');
|
|
122
122
|
}
|
|
123
|
-
if (options.
|
|
124
|
-
throw new Error('--
|
|
123
|
+
if (options.checkProof != null && options.proof) {
|
|
124
|
+
throw new Error('--check-proof cannot be combined with --proof or --proof-detail');
|
|
125
125
|
}
|
|
126
|
-
if (options.
|
|
127
|
-
throw new Error('--
|
|
126
|
+
if (options.checkProof != null && options.goals.length > 0) {
|
|
127
|
+
throw new Error('--check-proof cannot be combined with --goal');
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
if (options.isoStrict && options.files.length === 0 && options.goals.length === 0 &&
|
|
131
|
-
options.
|
|
131
|
+
options.checkProof == null && !options.proof && !options.quiet && !options.stats && !options.warnings) {
|
|
132
132
|
const engine = await loadEngine();
|
|
133
133
|
const { runRepl } = await import('./repl.js');
|
|
134
134
|
const exitCode = await runRepl(engine, {
|
|
@@ -172,7 +172,7 @@ export async function main(argv) {
|
|
|
172
172
|
sourceParts.push({ text: '', filename: '<empty>' });
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
if (options.goals.length === 0 && !options.quads && options.
|
|
175
|
+
if (options.goals.length === 0 && !options.quads && options.checkProof == null) {
|
|
176
176
|
for (const source of sourceParts) options.goals.push(...goalsFromSource(source.text));
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -188,7 +188,7 @@ export async function main(argv) {
|
|
|
188
188
|
|
|
189
189
|
const engine = await loadEngine();
|
|
190
190
|
let program = engine.Program.parseSources(sourceParts, {
|
|
191
|
-
sourceMetadata: options.proof || options.
|
|
191
|
+
sourceMetadata: options.proof || options.checkProof != null || options.isoStrict,
|
|
192
192
|
isoStrict: options.isoStrict,
|
|
193
193
|
autoload: options.autoload,
|
|
194
194
|
autoloadGoals: options.goals,
|
|
@@ -198,7 +198,7 @@ export async function main(argv) {
|
|
|
198
198
|
// A bare `?- Goal.` asks its question the same way a `%% ?-` comment
|
|
199
199
|
// does, but only the parser can find it, so it is picked up here rather
|
|
200
200
|
// than from the source text.
|
|
201
|
-
if (options.goals.length === 0 && !options.quads && options.
|
|
201
|
+
if (options.goals.length === 0 && !options.quads && options.checkProof == null && program.queries.length > 0) {
|
|
202
202
|
options.goals.push(...program.queries.map((query) => query.goal));
|
|
203
203
|
program = engine.autoloadProgramGoals(program, options.goals, { autoload: options.autoload });
|
|
204
204
|
}
|
|
@@ -214,16 +214,16 @@ export async function main(argv) {
|
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
if (options.
|
|
217
|
+
if (options.checkProof != null) {
|
|
218
218
|
const { checkProofDocument, verdict } = await import('./check-proof.js');
|
|
219
|
-
const proofText = await fs.readFile(options.
|
|
219
|
+
const proofText = await fs.readFile(options.checkProof, 'utf8');
|
|
220
220
|
const report = checkProofDocument(program, proofText);
|
|
221
|
-
if (report.steps === 0) throw new Error(`no step/4 proof step found in ${options.
|
|
221
|
+
if (report.steps === 0) throw new Error(`no step/4 proof step found in ${options.checkProof}`);
|
|
222
222
|
if (!report.valid) {
|
|
223
223
|
for (const failure of report.failures.slice(0, 5)) {
|
|
224
224
|
process.stderr.write(` [${failure.condition}] ${failure.conclusion} -- ${failure.detail}\n`);
|
|
225
225
|
}
|
|
226
|
-
throw new Error(`${options.
|
|
226
|
+
throw new Error(`${options.checkProof} is not a valid proof for this program: ${report.failures.length} failure(s)`);
|
|
227
227
|
}
|
|
228
228
|
process.stdout.write(`${verdict(report)}.\n`);
|
|
229
229
|
return;
|
|
@@ -317,10 +317,23 @@ async function runDefault(engine, program, options) {
|
|
|
317
317
|
});
|
|
318
318
|
if (options.proof && !options.quiet) {
|
|
319
319
|
const explanation = await loadExplanation();
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
const detail = options?.proofDetail ?? 'abstract';
|
|
321
|
+
const roots = [];
|
|
322
|
+
const unexplained = [];
|
|
323
|
+
for (const fact of claimed) {
|
|
324
|
+
const node = explanation.proofNodeFor(program, fact, { registry, proofDetail: detail, solver });
|
|
325
|
+
if (node) roots.push(node);
|
|
326
|
+
else unexplained.push(fact);
|
|
327
|
+
}
|
|
323
328
|
const { clauses, steps } = explanation.flattenProof(roots, program);
|
|
329
|
+
// An answer the explanation replay cannot reproduce is recorded as
|
|
330
|
+
// `unproven` rather than left without a step: a document containing
|
|
331
|
+
// one is not a valid proof, and saying so is the point.
|
|
332
|
+
const concluded = new Set(steps.map((step) => engine.termToString(step.conclusion, new engine.Env(), true)));
|
|
333
|
+
for (const fact of unexplained) {
|
|
334
|
+
if (concluded.has(engine.termToString(fact, new engine.Env(), true))) continue;
|
|
335
|
+
steps.push({ conclusion: fact, by: engine.atom('unproven'), bindings: [], uses: [] });
|
|
336
|
+
}
|
|
324
337
|
process.stdout.write(engine.proofBlocks(program, clauses, steps));
|
|
325
338
|
}
|
|
326
339
|
if (haltCode != null) process.exitCode = haltCode;
|
|
@@ -347,7 +360,7 @@ Options:
|
|
|
347
360
|
-h, --help Show this help text and exit.
|
|
348
361
|
-p, --proof Enable proof explanations.
|
|
349
362
|
--proof-detail mode Use abstract or expanded proof detail (implies --proof).
|
|
350
|
-
--
|
|
363
|
+
--check-proof file Check a saved proof document against the input program.
|
|
351
364
|
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
352
365
|
Note: -q is quads, not quiet; --quiet has no short form.
|
|
353
366
|
--quiet Suppress answer terms while preserving Prolog output.
|
package/src/explain.js
CHANGED
|
@@ -41,6 +41,19 @@ export function explainProof(program, goal, options = {}) {
|
|
|
41
41
|
return whyProof(program, goal, options);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// The solver a replay evaluates through.
|
|
45
|
+
//
|
|
46
|
+
// A constraint library keeps state on the solver instance that running a
|
|
47
|
+
// program's directives again does not rebuild -- CLP(B) answers its goals
|
|
48
|
+
// only through the solver that posted the constraints. So when the engine is
|
|
49
|
+
// explaining its own run, it explains through that solver; a caller with none
|
|
50
|
+
// gets a fresh one, which is enough for everything that is pure resolution.
|
|
51
|
+
let liveSolver = null;
|
|
52
|
+
|
|
53
|
+
function hostSolver(program, registry) {
|
|
54
|
+
return liveSolver && liveSolver.program === program ? liveSolver : new Solver(program, { registry });
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, detail) {
|
|
45
58
|
if (depth > maxDepth) return;
|
|
46
59
|
|
|
@@ -90,7 +103,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
90
103
|
// ordinary clauses, but explanations collapse its private helper expansion
|
|
91
104
|
// behind an explicit library(Name, Arity) boundary.
|
|
92
105
|
if (detail !== 'expanded' && group.module !== 'user' && program.modules.get(group.module)?.filename?.startsWith('src/lib/')) {
|
|
93
|
-
const solver =
|
|
106
|
+
const solver = hostSolver(program, registry);
|
|
94
107
|
for (const next of solver.solve([goal], env.clone(), 0)) {
|
|
95
108
|
const proofEnv = next.clone ? next.clone() : next;
|
|
96
109
|
yield {
|
|
@@ -199,7 +212,7 @@ function builtinDefinition(program, goal, env, registry) {
|
|
|
199
212
|
const def = registry.get(goal.name, goal.arity);
|
|
200
213
|
if (!def) return { handled: false, def: null, solver: null };
|
|
201
214
|
|
|
202
|
-
const solver =
|
|
215
|
+
const solver = hostSolver(program, registry);
|
|
203
216
|
if (!builtinIsUsedForGoal(def, solver, goal, env)) return { handled: false, def: null, solver: null };
|
|
204
217
|
return { handled: true, def, solver };
|
|
205
218
|
}
|
|
@@ -812,10 +825,18 @@ export function flattenProof(roots, program) {
|
|
|
812
825
|
|
|
813
826
|
// The root of an answer's proof tree, for `flattenProof`.
|
|
814
827
|
export function proofNodeFor(program, goal, options = {}) {
|
|
828
|
+
liveSolver = options.solver ?? null;
|
|
815
829
|
const maxDepth = options.maxDepth ?? 256;
|
|
816
830
|
const registry = options.registry ?? getEyePrologRegistry();
|
|
817
831
|
const env = options.env ?? new Env();
|
|
818
832
|
const detail = normalizeProofDetail(options.proofDetail ?? 'abstract');
|
|
819
|
-
|
|
833
|
+
try {
|
|
834
|
+
for (const proof of proveGoalAll(program, goal, env, 0, maxDepth, registry, [], detail)) return proof.node;
|
|
835
|
+
} catch {
|
|
836
|
+
// A replay that raises has not explained anything, and an answer the
|
|
837
|
+
// engine found should not be lost because explaining it failed. The
|
|
838
|
+
// caller records the answer as `unproven`, which says exactly that.
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
820
841
|
return null;
|
|
821
842
|
}
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { getStrictIsoRegistry } from './iso.js';
|
|
|
40
40
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
41
41
|
import { executeForwardRules, executeGoals, hasForwardRules, normalizeGoals } from './execute.js';
|
|
42
42
|
import { proofBlocks } from './result-format.js';
|
|
43
|
+
import { Env, atom, termToString } from './term.js';
|
|
43
44
|
|
|
44
45
|
// The public API is an entry point above the solver/registry layers, so it can
|
|
45
46
|
// install pruning-aware iterator disposal without introducing an import cycle.
|
|
@@ -89,7 +90,7 @@ export function run(source, options = {}) {
|
|
|
89
90
|
options.ioOptions?.errorWrite?.(line);
|
|
90
91
|
},
|
|
91
92
|
}));
|
|
92
|
-
if (includeWhy) output.push(proofBlocksFor(program, derived, runOptions.registry, options.proofDetail));
|
|
93
|
+
if (includeWhy) output.push(proofBlocksFor(program, derived, runOptions.registry, options.proofDetail, solver));
|
|
93
94
|
} else {
|
|
94
95
|
// A bare `?- Goal.` asks its question the same way a `%% ?-` comment
|
|
95
96
|
// does; only the parser can find it, so it is picked up here.
|
|
@@ -102,7 +103,7 @@ export function run(source, options = {}) {
|
|
|
102
103
|
claimed.push(resolved);
|
|
103
104
|
},
|
|
104
105
|
}));
|
|
105
|
-
if (includeWhy) output.push(proofBlocksFor(program, claimed, runOptions.registry, options.proofDetail));
|
|
106
|
+
if (includeWhy) output.push(proofBlocksFor(program, claimed, runOptions.registry, options.proofDetail, solver));
|
|
106
107
|
}
|
|
107
108
|
return { stdout: output.join(''), stats: solver.stats, haltCode };
|
|
108
109
|
}
|
|
@@ -110,9 +111,25 @@ export function run(source, options = {}) {
|
|
|
110
111
|
// The `clause/3` and `step/4` blocks explaining the facts a run claimed.
|
|
111
112
|
// One walk across every claim, so a conclusion several of them rest on is
|
|
112
113
|
// explained once.
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
//
|
|
115
|
+
// An answer the solver found but the explanation replay cannot reproduce --
|
|
116
|
+
// a CLP(B) answer decided by propagation rather than by resolution -- is
|
|
117
|
+
// recorded as `unproven` rather than quietly left without a step. A document
|
|
118
|
+
// containing one is not a valid proof, and saying so is the point.
|
|
119
|
+
function proofBlocksFor(program, claimed, registry, proofDetail = 'abstract', solver = null) {
|
|
120
|
+
const roots = [];
|
|
121
|
+
const unexplained = [];
|
|
122
|
+
for (const fact of claimed) {
|
|
123
|
+
const node = proofNodeFor(program, fact, { registry, proofDetail, solver });
|
|
124
|
+
if (node) roots.push(node);
|
|
125
|
+
else unexplained.push(fact);
|
|
126
|
+
}
|
|
115
127
|
const { clauses, steps } = flattenProof(roots, program);
|
|
128
|
+
const concluded = new Set(steps.map((step) => termToString(step.conclusion, new Env(), true)));
|
|
129
|
+
for (const fact of unexplained) {
|
|
130
|
+
if (concluded.has(termToString(fact, new Env(), true))) continue;
|
|
131
|
+
steps.push({ conclusion: fact, by: atom('unproven'), bindings: [], uses: [] });
|
|
132
|
+
}
|
|
116
133
|
return proofBlocks(program, clauses, steps);
|
|
117
134
|
}
|
|
118
135
|
|
|
@@ -4338,7 +4338,7 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4338
4338
|
},
|
|
4339
4339
|
},
|
|
4340
4340
|
{
|
|
4341
|
-
name: '--
|
|
4341
|
+
name: '--check-proof re-performs a saved proof and rejects tampering',
|
|
4342
4342
|
run: () => {
|
|
4343
4343
|
const programFile = path.join(temp.dir, `proof-program-${++temp.counter}.pl`);
|
|
4344
4344
|
const proofFile = path.join(temp.dir, `proof-certificate-${++temp.counter}.pl`);
|
|
@@ -4346,15 +4346,15 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4346
4346
|
const generated = runCli(['--proof', programFile]);
|
|
4347
4347
|
assertEqual(generated.status, 0, 'proof generation status');
|
|
4348
4348
|
fs.writeFileSync(proofFile, generated.stdout);
|
|
4349
|
-
const verified = runCli(['--
|
|
4349
|
+
const verified = runCli(['--check-proof', proofFile, programFile]);
|
|
4350
4350
|
assertEqual(verified.status, 0, 'verification status');
|
|
4351
4351
|
assertEqual(verified.stdout, 'checked: 2 steps.\n', 'verification stdout');
|
|
4352
|
-
const strictVerified = runCli(['--iso-strict', '--
|
|
4352
|
+
const strictVerified = runCli(['--iso-strict', '--check-proof', proofFile, programFile]);
|
|
4353
4353
|
assertEqual(strictVerified.status, 0, 'strict verification status');
|
|
4354
4354
|
assertEqual(strictVerified.stdout, 'checked: 2 steps.\n', 'strict verification stdout');
|
|
4355
4355
|
const tamperedFile = path.join(temp.dir, `proof-certificate-bad-${++temp.counter}.pl`);
|
|
4356
4356
|
fs.writeFileSync(tamperedFile, generated.stdout.replace('step(p(a),', 'step(p(b),'));
|
|
4357
|
-
const rejected = runCli(['--
|
|
4357
|
+
const rejected = runCli(['--check-proof', tamperedFile, programFile]);
|
|
4358
4358
|
assertEqual(rejected.status, 1, 'tampered verification status');
|
|
4359
4359
|
assertIncludes(rejected.stderr, 'is not a valid proof for this program', 'tampered verification stderr');
|
|
4360
4360
|
},
|
|
@@ -19,22 +19,37 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
19
19
|
const examplesDir = path.join(root, 'examples');
|
|
20
20
|
|
|
21
21
|
/// Proofs that do not check, and what each one exposes. An entry here is a
|
|
22
|
-
/// gap in proof *generation*, not in the checker:
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
///
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
/// gap in proof *generation*, not in the checker: the explanation replay
|
|
23
|
+
/// cannot reproduce the answer, so the writer records it as `unproven` and
|
|
24
|
+
/// says so rather than leaving it out.
|
|
25
|
+
///
|
|
26
|
+
/// Explaining through the solver that produced the answer closed the three
|
|
27
|
+
/// CLP(B) gaps that used to be here, so the list is empty: every packaged
|
|
28
|
+
/// proof checks, and this test fails if one stops doing so.
|
|
29
|
+
///
|
|
30
|
+
/// The CLP(Z) examples are not in this corpus, and not because they fail:
|
|
31
|
+
/// `clpz`'s goal expansion names the variables it introduces from a counter
|
|
32
|
+
/// that advances across a run, so a `clause/3` record of an expanded body
|
|
33
|
+
/// differs between runs and cannot be compared against a golden. Checked
|
|
34
|
+
/// directly rather than through a golden, `clpz-factorial`, `clpz-n-queens`,
|
|
35
|
+
/// `clpz-global-constraints` and `clpz-sudoku-9x9` all check.
|
|
36
|
+
const KNOWN_GAPS = new Map([]);
|
|
37
|
+
|
|
38
|
+
const totals = { steps: 0, verified: 0, trusted: 0 };
|
|
31
39
|
|
|
32
40
|
export function runProofChecking(reporter = new TestReporter()) {
|
|
33
41
|
reporter.section('Proof checking');
|
|
42
|
+
totals.steps = 0;
|
|
43
|
+
totals.verified = 0;
|
|
44
|
+
totals.trusted = 0;
|
|
34
45
|
for (const name of [...proofExamples].sort()) {
|
|
35
46
|
reporter.test(name, () => checkPackagedProof(name));
|
|
36
47
|
}
|
|
37
48
|
reporter.sectionTotal('proof checking');
|
|
49
|
+
// What the corpus establishes, in the terms the specification uses: a
|
|
50
|
+
// verified step was re-performed, a trusted one was recorded because
|
|
51
|
+
// re-deciding it would mean running the program.
|
|
52
|
+
reporter.section(`${totals.steps} steps, ${totals.verified} verified, ${totals.trusted} trusted`);
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
function checkPackagedProof(name) {
|
|
@@ -42,6 +57,9 @@ function checkPackagedProof(name) {
|
|
|
42
57
|
const proof = fs.readFileSync(path.join(examplesDir, 'proof', name), 'utf8');
|
|
43
58
|
const program = Program.parseSources([{ text: source, filename: name }], { sourceMetadata: true });
|
|
44
59
|
const report = checkProofDocument(program, proof);
|
|
60
|
+
totals.steps += report.steps;
|
|
61
|
+
totals.verified += report.verified;
|
|
62
|
+
totals.trusted += report.trusted.length;
|
|
45
63
|
const gap = KNOWN_GAPS.get(name);
|
|
46
64
|
if (gap) {
|
|
47
65
|
assertEqual(report.valid, false, `${name} now checks; remove it from KNOWN_GAPS (${gap})`);
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1494,7 +1494,7 @@ output is valid EyeProlog input and can be kept and checked later:
|
|
|
1494
1494
|
|
|
1495
1495
|
```sh
|
|
1496
1496
|
eyeprolog --proof examples/socrates.pl > socrates.why.pl
|
|
1497
|
-
eyeprolog --
|
|
1497
|
+
eyeprolog --check-proof socrates.why.pl examples/socrates.pl
|
|
1498
1498
|
```
|
|
1499
1499
|
|
|
1500
1500
|
The second command re-performs every inference the document records against
|
|
@@ -9589,7 +9589,7 @@ explicit.
|
|
|
9589
9589
|
| `-h`, `--help` | Show usage |
|
|
9590
9590
|
| `-p`, `--proof` | Print `why/2` explanations |
|
|
9591
9591
|
| `--proof-detail abstract|expanded` | Select library abstraction for proof output; implies `--proof` |
|
|
9592
|
-
| `--
|
|
9592
|
+
| `--check-proof File` | Verify saved `why/2` proof certificates against the input program without proof search |
|
|
9593
9593
|
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
9594
9594
|
| `--quiet` | Suppress resolved answer terms while preserving Prolog output and diagnostics |
|
|
9595
9595
|
| `--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 |
|
|
@@ -9636,7 +9636,7 @@ Work in a fixed sequence:
|
|
|
9636
9636
|
1. predict the ground answers before running the program;
|
|
9637
9637
|
2. run without observation flags and compare stdout with that prediction;
|
|
9638
9638
|
3. add `--proof` when the support for an answer is the question; save the
|
|
9639
|
-
output and use `--
|
|
9639
|
+
output and use `--check-proof` when the derivation itself must cross a
|
|
9640
9640
|
process or review boundary;
|
|
9641
9641
|
4. add `--warnings` when portability or negative dependencies are the
|
|
9642
9642
|
question; use `--portable` when non-profile dependencies must fail CI;
|
|
@@ -9648,7 +9648,7 @@ For example:
|
|
|
9648
9648
|
eyeprolog --goal 'ancestor(X, Y)' examples/ancestor.pl
|
|
9649
9649
|
eyeprolog --proof --goal 'type(X, Y)' examples/socrates.pl
|
|
9650
9650
|
eyeprolog --proof examples/socrates.pl > socrates.why.pl
|
|
9651
|
-
eyeprolog --
|
|
9651
|
+
eyeprolog --check-proof socrates.why.pl examples/socrates.pl
|
|
9652
9652
|
eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
|
|
9653
9653
|
eyeprolog --portable --goal 'sudoku9(S)' examples/clpz-sudoku-9x9.pl
|
|
9654
9654
|
eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
|