eyeprolog 1.6.15 → 1.6.16
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/package.json +1 -1
- package/src/check-proof.js +53 -1
- package/src/cli.js +9 -2
- package/test/regression/cases-regression.mjs +15 -2
- package/the-art-of-eyeprolog.md +8 -4
package/package.json
CHANGED
package/src/check-proof.js
CHANGED
|
@@ -351,8 +351,10 @@ export function checkProofDocument(program, text) {
|
|
|
351
351
|
failures.push({ condition: 'C4', conclusion: key(claim), detail: 'claimed, but no step concludes it' });
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
+
let uses = 0;
|
|
354
355
|
for (const step of steps) {
|
|
355
356
|
for (const use of step.uses) {
|
|
357
|
+
uses++;
|
|
356
358
|
if (byConclusion.has(key(use)) || given.has(key(use))) continue;
|
|
357
359
|
failures.push({ condition: 'C4', conclusion: key(step.conclusion), detail: `uses ${key(use)}, which nothing concludes and the program does not give` });
|
|
358
360
|
}
|
|
@@ -360,7 +362,57 @@ export function checkProofDocument(program, text) {
|
|
|
360
362
|
|
|
361
363
|
checkWellFounded(byConclusion, failures);
|
|
362
364
|
|
|
363
|
-
|
|
365
|
+
const failed = (condition) => failures.filter((failure) => failure.condition === condition).length;
|
|
366
|
+
// What each condition covered, so a reader can see the shape of the check
|
|
367
|
+
// rather than only its verdict. A condition that examined nothing says so:
|
|
368
|
+
// "0 steps" is information, not a pass.
|
|
369
|
+
const conditions = [
|
|
370
|
+
{ id: 'C1', name: 'Resolution', covered: verified, failed: failed('C1'),
|
|
371
|
+
summary: `${verified} step(s) re-performed against the source clause they cite` },
|
|
372
|
+
{ id: 'C2', name: 'Well-founded', covered: steps.length, failed: failed('C2'),
|
|
373
|
+
summary: `${steps.length} step(s) checked for a conclusion resting on its own derivation` },
|
|
374
|
+
{ id: 'C3', name: 'Justification', covered: steps.length, failed: failed('C3'),
|
|
375
|
+
summary: `${steps.length} step(s) carrying exactly one known justification` },
|
|
376
|
+
{ id: 'C4', name: 'Coverage', covered: claims.length + uses, failed: failed('C4'),
|
|
377
|
+
summary: `${claims.length} claim(s) and ${uses} use(s) resolved to a step or to a statement the program gives` },
|
|
378
|
+
{ id: 'C5', name: 'Re-decision', covered: redecided, failed: failed('C5'),
|
|
379
|
+
summary: `${redecided} step(s) recomputed against a program holding no clause of the theory` },
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
valid: failures.length === 0,
|
|
384
|
+
steps: steps.length,
|
|
385
|
+
verified,
|
|
386
|
+
redecided,
|
|
387
|
+
uses,
|
|
388
|
+
trusted,
|
|
389
|
+
failures,
|
|
390
|
+
claims: claims.length,
|
|
391
|
+
conditions,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// The condition-by-condition account, as lines. The obligations are listed
|
|
396
|
+
// under C5 because that is the condition they escaped: a step recorded as
|
|
397
|
+
// `builtin` that recomputation could not decide, or one whose justification
|
|
398
|
+
// puts it outside recomputation altogether.
|
|
399
|
+
export function conditionReport(report) {
|
|
400
|
+
const lines = [];
|
|
401
|
+
for (const condition of report.conditions ?? []) {
|
|
402
|
+
const mark = condition.failed > 0 ? `${condition.failed} failure(s)` : 'ok';
|
|
403
|
+
lines.push(` ${condition.id} ${condition.name.padEnd(13)} ${mark.padEnd(14)} ${condition.summary}`);
|
|
404
|
+
}
|
|
405
|
+
const obligations = report.trusted ?? [];
|
|
406
|
+
if (obligations.length > 0) {
|
|
407
|
+
const byReason = new Map();
|
|
408
|
+
for (const item of obligations) {
|
|
409
|
+
const reason = item.reason ?? item.kind;
|
|
410
|
+
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
|
|
411
|
+
}
|
|
412
|
+
const parts = [...byReason].map(([reason, count]) => `${count} ${reason}`).join(', ');
|
|
413
|
+
lines.push(` -- ${obligations.length} obligation(s) the check rests on rather than establishes: ${parts}`);
|
|
414
|
+
}
|
|
415
|
+
return lines;
|
|
364
416
|
}
|
|
365
417
|
|
|
366
418
|
// The program's clauses, numbered from 1 in load order -- the numbering
|
package/src/cli.js
CHANGED
|
@@ -252,16 +252,20 @@ export async function main(argv) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
if (options.checkProof != null) {
|
|
255
|
-
const { checkProofDocument, verdict } = await import('./check-proof.js');
|
|
255
|
+
const { checkProofDocument, conditionReport, verdict } = await import('./check-proof.js');
|
|
256
256
|
const proofText = await fs.readFile(options.checkProof, 'utf8');
|
|
257
257
|
const report = checkProofDocument(program, proofText);
|
|
258
258
|
if (report.steps === 0) throw new Error(`no step/4 proof step found in ${options.checkProof}`);
|
|
259
259
|
if (!report.valid) {
|
|
260
|
+
// The conditions go to stderr alongside the failures, so a failing run
|
|
261
|
+
// still says which of the five objected and which held.
|
|
262
|
+
for (const line of conditionReport(report)) process.stderr.write(`${line}\n`);
|
|
260
263
|
for (const failure of report.failures.slice(0, 5)) {
|
|
261
264
|
process.stderr.write(` [${failure.condition}] ${failure.conclusion} -- ${failure.detail}\n`);
|
|
262
265
|
}
|
|
263
266
|
throw new Error(`${options.checkProof} is not a valid proof for this program: ${report.failures.length} failure(s)`);
|
|
264
267
|
}
|
|
268
|
+
if (!options.quiet) for (const line of conditionReport(report)) process.stdout.write(`${line}\n`);
|
|
265
269
|
process.stdout.write(`${verdict(report)}.\n`);
|
|
266
270
|
return;
|
|
267
271
|
}
|
|
@@ -396,7 +400,10 @@ Input:
|
|
|
396
400
|
Options:
|
|
397
401
|
-h, --help Show this help text and exit.
|
|
398
402
|
-p, --proof Enable proof explanations.
|
|
399
|
-
--proof-detail mode
|
|
403
|
+
--proof-detail mode abstract stops at a bundled library predicate and records
|
|
404
|
+
it as one builtin step; expanded explains through it,
|
|
405
|
+
as ordinary source steps. Only differs for a program
|
|
406
|
+
that calls a library. (implies --proof)
|
|
400
407
|
--check-proof file Check a saved proof document against the input program.
|
|
401
408
|
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
402
409
|
Note: -q is quads, not quiet; --quiet has no short form.
|
|
@@ -4348,15 +4348,28 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4348
4348
|
fs.writeFileSync(proofFile, generated.stdout);
|
|
4349
4349
|
const verified = runCli(['--check-proof', proofFile, programFile]);
|
|
4350
4350
|
assertEqual(verified.status, 0, 'verification status');
|
|
4351
|
-
|
|
4351
|
+
// The run accounts for all five conditions and then gives its verdict,
|
|
4352
|
+
// so a reader sees which conditions the result rests on rather than
|
|
4353
|
+
// only that it passed.
|
|
4354
|
+
for (const condition of ['C1 Resolution', 'C2 Well-founded', 'C3 Justification', 'C4 Coverage', 'C5 Re-decision']) {
|
|
4355
|
+
assertIncludes(verified.stdout, condition, `verification reports ${condition}`);
|
|
4356
|
+
}
|
|
4357
|
+
assertEqual(verified.stdout.trimEnd().split('\n').pop(), 'checked: 2 steps.', 'verification verdict');
|
|
4358
|
+
assertEqual(verified.stdout.includes('obligation'), false, 'nothing was left trusted');
|
|
4359
|
+
const quiet = runCli(['--quiet', '--check-proof', proofFile, programFile]);
|
|
4360
|
+
assertEqual(quiet.stdout, 'checked: 2 steps.\n', 'quiet verification stdout is the verdict alone');
|
|
4352
4361
|
const strictVerified = runCli(['--iso-strict', '--check-proof', proofFile, programFile]);
|
|
4353
4362
|
assertEqual(strictVerified.status, 0, 'strict verification status');
|
|
4354
|
-
assertEqual(strictVerified.stdout, 'checked: 2 steps
|
|
4363
|
+
assertEqual(strictVerified.stdout.trimEnd().split('\n').pop(), 'checked: 2 steps.', 'strict verification verdict');
|
|
4355
4364
|
const tamperedFile = path.join(temp.dir, `proof-certificate-bad-${++temp.counter}.pl`);
|
|
4356
4365
|
fs.writeFileSync(tamperedFile, generated.stdout.replace('step(p(a),', 'step(p(b),'));
|
|
4357
4366
|
const rejected = runCli(['--check-proof', tamperedFile, programFile]);
|
|
4358
4367
|
assertEqual(rejected.status, 1, 'tampered verification status');
|
|
4359
4368
|
assertIncludes(rejected.stderr, 'is not a valid proof for this program', 'tampered verification stderr');
|
|
4369
|
+
// A failing run still accounts for every condition, so the report says
|
|
4370
|
+
// which one objected rather than only that something did.
|
|
4371
|
+
assertIncludes(rejected.stderr, 'C1 Resolution', 'tampered run still reports the conditions');
|
|
4372
|
+
assertIncludes(rejected.stderr, 'failure(s)', 'tampered run marks the objecting condition');
|
|
4360
4373
|
},
|
|
4361
4374
|
},
|
|
4362
4375
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1538,10 +1538,14 @@ Checking re-performs each step against the clause it cites: the clause must
|
|
|
1538
1538
|
exist, its variables bound as the step recorded them must yield exactly this
|
|
1539
1539
|
conclusion from exactly these uses, every claim must have a step, every use
|
|
1540
1540
|
must resolve to a step or to a statement the program gives, and no conclusion
|
|
1541
|
-
may rest on itself. `builtin
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1541
|
+
may rest on itself. A `builtin` step derives from no clause, so reading the
|
|
1542
|
+
document cannot decide it; it is computed again instead, against a program
|
|
1543
|
+
holding the bundled libraries and no clause of the theory under proof, and
|
|
1544
|
+
must agree. `absent`, `collected` and `asserted` steps stay trusted: the first
|
|
1545
|
+
two range over the theory that recomputation deliberately excludes, and a
|
|
1546
|
+
clause asserted at run time is in no source file to check against. The report
|
|
1547
|
+
counts what was verified, what was recomputed, and what remains an obligation
|
|
1548
|
+
rather than folding them into an undifferentiated success. An
|
|
1545
1549
|
answer the solver found but the explanation cannot reproduce is recorded as
|
|
1546
1550
|
`unproven`, which makes the document fail its check, because that is the
|
|
1547
1551
|
truth about it.
|