eyeprolog 1.5.21 → 1.5.23
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/conformance-report.md +2 -2
- 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/benchmark.mjs +61 -0
- package/test/conformance/ISO-COMPLIANCE.md +4 -4
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +1 -1
- package/test/conformance/README.md +1 -1
- package/test/conformance/WG17-SYNTAX-STATUS.md +9 -9
- package/test/conformance/expected/iso/logtalk_univ.pl +1 -1
- package/test/conformance/wg17-syntax-cases.json +4 -103
- package/test/conformance/wg17-syntax-coverage.json +3 -3
- package/test/run-regression.mjs +216 -5
- package/the-art-of-eyeprolog.md +68 -11
- package/tools/upgrade-wg17.mjs +26 -0
- package/why-eyeprolog.md +14 -3
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: () => {
|
|
@@ -5347,11 +5387,12 @@ ${profile}`;
|
|
|
5347
5387
|
assertEqual(report.total.positive + report.total.errors + report.total.warnings + report.total.proofs, report.total.total, 'conformance total');
|
|
5348
5388
|
assertEqual(report.rows.some((row) => row.category === 'legacy-numbered'), false, 'legacy-numbered category');
|
|
5349
5389
|
const wg17 = report.executable.find((gate) => gate.name === 'WG17 syntax');
|
|
5350
|
-
|
|
5351
|
-
assertEqual(wg17?.
|
|
5390
|
+
const wg17FixtureTotal = readWg17SyntaxFixture().cases.length;
|
|
5391
|
+
assertEqual(wg17?.total, wg17FixtureTotal, 'WG17 executable total');
|
|
5392
|
+
assertEqual(wg17?.passed, wg17FixtureTotal, 'WG17 executable passed');
|
|
5352
5393
|
assertArrayEqual(wg17?.failures ?? [], [], 'WG17 executable failures');
|
|
5353
5394
|
const text = formatConformanceReport(report);
|
|
5354
|
-
assertIncludes(text,
|
|
5395
|
+
assertIncludes(text, `| WG17 syntax | ${wg17FixtureTotal} | ${wg17FixtureTotal} | pass |`, 'report');
|
|
5355
5396
|
assertIncludes(text, '| variables |', 'report');
|
|
5356
5397
|
assertIncludes(text, '| Proofs |', 'report');
|
|
5357
5398
|
assertIncludes(text, '| **Total** |', 'report');
|
|
@@ -5624,6 +5665,74 @@ true :+ ready.
|
|
|
5624
5665
|
},
|
|
5625
5666
|
},
|
|
5626
5667
|
|
|
5668
|
+
{
|
|
5669
|
+
name: 'proof certificates verify source derivations without proof search',
|
|
5670
|
+
run: () => {
|
|
5671
|
+
const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
|
|
5672
|
+
const result = proofCertificate(program, parseGoalText('q(a, b)'));
|
|
5673
|
+
assertEqual(result.ok, true, 'certificate generated');
|
|
5674
|
+
assertEqual(result.certificate.version, 1, 'certificate version');
|
|
5675
|
+
assertEqual(result.certificate.answer, 'q(a, b)', 'certificate answer');
|
|
5676
|
+
const checked = verifyProof(program, result);
|
|
5677
|
+
assertEqual(checked.ok, true, 'certificate verifies');
|
|
5678
|
+
assertEqual(checked.trusted.length, 0, 'source-only certificate has no trusted boundaries');
|
|
5679
|
+
},
|
|
5680
|
+
},
|
|
5681
|
+
{
|
|
5682
|
+
name: 'proof verification rejects a tampered child goal',
|
|
5683
|
+
run: () => {
|
|
5684
|
+
const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
|
|
5685
|
+
const result = proofCertificate(program, parseGoalText('q(a, b)'));
|
|
5686
|
+
const tampered = structuredClone(result.certificate);
|
|
5687
|
+
tampered.proof.children[0].goal = 'p(a, c)';
|
|
5688
|
+
assertEqual(verifyProof(program, tampered).ok, false, 'tampered child is rejected');
|
|
5689
|
+
const bindingTamper = structuredClone(result.certificate);
|
|
5690
|
+
bindingTamper.proof.bindings[1].value = 'c';
|
|
5691
|
+
assertEqual(verifyProof(program, bindingTamper).ok, false, 'tampered binding is rejected');
|
|
5692
|
+
const changedProgram = Program.parse('p(a, c).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
|
|
5693
|
+
assertEqual(verifyProof(changedProgram, result).ok, false, 'changed source program is rejected');
|
|
5694
|
+
},
|
|
5695
|
+
},
|
|
5696
|
+
{
|
|
5697
|
+
name: 'why/2 text round-trips into a verifiable proof certificate',
|
|
5698
|
+
run: () => {
|
|
5699
|
+
const program = Program.parse('p(a).\nq(X) :- p(X).\n', { sourceMetadata: true });
|
|
5700
|
+
const result = proofCertificate(program, parseGoalText('q(a)'));
|
|
5701
|
+
const parsed = proofCertificatesFromText(result.text, program);
|
|
5702
|
+
assertEqual(parsed.length, 1, 'certificate count');
|
|
5703
|
+
assertEqual(verifyProof(program, parsed[0]).ok, true, 'round-tripped certificate verifies');
|
|
5704
|
+
},
|
|
5705
|
+
},
|
|
5706
|
+
{
|
|
5707
|
+
name: 'proof certificate text round-trips under every double_quotes mode',
|
|
5708
|
+
run: () => {
|
|
5709
|
+
for (const doubleQuotes of ['chars', 'codes', 'atom']) {
|
|
5710
|
+
const program = Program.parse('p(a).\nq(X) :- p(X).\n', { sourceMetadata: true, doubleQuotes });
|
|
5711
|
+
const result = proofCertificate(program, parseGoalText('q(a)', { doubleQuotes }));
|
|
5712
|
+
const parsed = proofCertificatesFromText(result.text, program);
|
|
5713
|
+
assertEqual(parsed.length, 1, `${doubleQuotes} certificate count`);
|
|
5714
|
+
assertEqual(verifyProof(program, parsed[0]).ok, true, `${doubleQuotes} certificate verifies`);
|
|
5715
|
+
}
|
|
5716
|
+
},
|
|
5717
|
+
},
|
|
5718
|
+
{
|
|
5719
|
+
name: 'expanded proof detail opens bundled Prolog library clauses',
|
|
5720
|
+
run: () => {
|
|
5721
|
+
const program = Program.parse(':- use_module(library(lists)).\nq(X) :- member(X, [a,b]).\n', { sourceMetadata: true });
|
|
5722
|
+
const abstract = proofCertificate(program, parseGoalText('q(a)'));
|
|
5723
|
+
const expanded = proofCertificate(program, parseGoalText('q(a)'), { proofDetail: 'expanded' });
|
|
5724
|
+
assertEqual(abstract.certificate.proof.children[0].method.type, 'library', 'abstract library boundary');
|
|
5725
|
+
assertEqual(expanded.certificate.proof.children[0].method.type, 'source', 'expanded library source');
|
|
5726
|
+
const abstractChecked = verifyProof(program, abstract);
|
|
5727
|
+
const expandedChecked = verifyProof(program, expanded);
|
|
5728
|
+
assertEqual(abstractChecked.ok, true, 'abstract certificate verifies');
|
|
5729
|
+
assertEqual(abstractChecked.trusted.length, 1, 'abstract certificate reports library trust');
|
|
5730
|
+
assertEqual(abstractChecked.trusted[0].type, 'library', 'reported trust boundary type');
|
|
5731
|
+
assertEqual(expandedChecked.ok, true, 'expanded certificate verifies');
|
|
5732
|
+
assertEqual(expandedChecked.trusted.length, 0, 'pure-Prolog expanded certificate removes library trust');
|
|
5733
|
+
},
|
|
5734
|
+
},
|
|
5735
|
+
|
|
5627
5736
|
{
|
|
5628
5737
|
name: 'run accepts Program instances',
|
|
5629
5738
|
run: () => {
|
|
@@ -7171,6 +7280,92 @@ function whiteBoxCases() {
|
|
|
7171
7280
|
'double_quotes(codes) representation');
|
|
7172
7281
|
},
|
|
7173
7282
|
},
|
|
7283
|
+
{
|
|
7284
|
+
name: 'double-quoted character and code suffixes are recognized inside proper lists (issue #88 suffix follow-up)',
|
|
7285
|
+
run: () => {
|
|
7286
|
+
const repl = runCli([], {
|
|
7287
|
+
input: 'T = [A,b,c,d,e,f].\nhalt.\n',
|
|
7288
|
+
});
|
|
7289
|
+
assertEqual(repl.status, 0, 'REPL exit status');
|
|
7290
|
+
assertIncludes(repl.stdout, 'T = [A|"bcdef"].', 'Scryer-compatible character suffix');
|
|
7291
|
+
assertNotIncludes(repl.stdout, 'T = [A, b, c, d, e, f].', 'expanded suffix suppressed');
|
|
7292
|
+
|
|
7293
|
+
const chars = parseGoalText('p([foo,b,c])').args[0];
|
|
7294
|
+
assertEqual(
|
|
7295
|
+
formatTermForWrite(chars, new Env(), {
|
|
7296
|
+
quoted: true,
|
|
7297
|
+
doubleQuotes: 'chars',
|
|
7298
|
+
doubleBar: true,
|
|
7299
|
+
compact: true,
|
|
7300
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7301
|
+
}),
|
|
7302
|
+
'[foo|"bc"]',
|
|
7303
|
+
'ordinary list notation uses a quoted suffix',
|
|
7304
|
+
);
|
|
7305
|
+
assertEqual(
|
|
7306
|
+
formatTermForWrite(chars, new Env(), {
|
|
7307
|
+
quoted: true,
|
|
7308
|
+
ignoreOps: true,
|
|
7309
|
+
doubleQuotes: 'chars',
|
|
7310
|
+
doubleBar: true,
|
|
7311
|
+
compact: true,
|
|
7312
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7313
|
+
}),
|
|
7314
|
+
`'.'(foo,"bc")`,
|
|
7315
|
+
'ignore_ops remains orthogonal to suffix quoting',
|
|
7316
|
+
);
|
|
7317
|
+
assertEqual(
|
|
7318
|
+
formatTermForWrite(chars, new Env(), {
|
|
7319
|
+
quoted: true,
|
|
7320
|
+
doubleQuotes: null,
|
|
7321
|
+
doubleBar: true,
|
|
7322
|
+
compact: true,
|
|
7323
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7324
|
+
}),
|
|
7325
|
+
'[foo,b,c]',
|
|
7326
|
+
'disabled double_quotes keeps expanded list notation',
|
|
7327
|
+
);
|
|
7328
|
+
const improper = parseGoalText('p([foo,b,c|tail])').args[0];
|
|
7329
|
+
assertEqual(
|
|
7330
|
+
formatTermForWrite(improper, new Env(), {
|
|
7331
|
+
quoted: true,
|
|
7332
|
+
doubleQuotes: 'chars',
|
|
7333
|
+
doubleBar: true,
|
|
7334
|
+
compact: true,
|
|
7335
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7336
|
+
}),
|
|
7337
|
+
'[foo,b,c|tail]',
|
|
7338
|
+
'improper suffix is not mistaken for a proper string suffix',
|
|
7339
|
+
);
|
|
7340
|
+
|
|
7341
|
+
const astral = parseGoalText("p([foo,'😀',x])").args[0];
|
|
7342
|
+
assertEqual(
|
|
7343
|
+
formatTermForWrite(astral, new Env(), {
|
|
7344
|
+
quoted: true,
|
|
7345
|
+
doubleQuotes: 'chars',
|
|
7346
|
+
doubleBar: true,
|
|
7347
|
+
compact: true,
|
|
7348
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7349
|
+
}),
|
|
7350
|
+
'[foo|"😀x"]',
|
|
7351
|
+
'Unicode scalar suffix',
|
|
7352
|
+
);
|
|
7353
|
+
|
|
7354
|
+
const codes = parseGoalText('p([foo,98,128512])', { doubleQuotes: 'codes' }).args[0];
|
|
7355
|
+
assertEqual(
|
|
7356
|
+
formatTermForWrite(codes, new Env(), {
|
|
7357
|
+
quoted: true,
|
|
7358
|
+
doubleQuotes: 'codes',
|
|
7359
|
+
doubleBar: true,
|
|
7360
|
+
compact: true,
|
|
7361
|
+
operators: ISO_OPERATOR_DEFINITIONS,
|
|
7362
|
+
}),
|
|
7363
|
+
'[foo|"b😀"]',
|
|
7364
|
+
'code-list suffix',
|
|
7365
|
+
);
|
|
7366
|
+
},
|
|
7367
|
+
},
|
|
7368
|
+
|
|
7174
7369
|
{
|
|
7175
7370
|
name: 'charsio write_term_to_chars composes double_quotes with ignore_ops',
|
|
7176
7371
|
run: () => {
|
|
@@ -8148,6 +8343,7 @@ function documentedConformanceMetricIssues() {
|
|
|
8148
8343
|
const report = buildConformanceReport();
|
|
8149
8344
|
const iso = report.rows.find((row) => row.category === 'iso')?.total;
|
|
8150
8345
|
const total = report.total.total;
|
|
8346
|
+
const wg17 = report.executable.find((gate) => gate.name === 'WG17 syntax')?.total;
|
|
8151
8347
|
const checks = [
|
|
8152
8348
|
{
|
|
8153
8349
|
file: path.join(packageRoot, 'the-art-of-eyeprolog.md'),
|
|
@@ -8177,6 +8373,21 @@ function documentedConformanceMetricIssues() {
|
|
|
8177
8373
|
}
|
|
8178
8374
|
}
|
|
8179
8375
|
}
|
|
8376
|
+
for (const file of [
|
|
8377
|
+
path.join(packageRoot, 'test', 'conformance', 'README.md'),
|
|
8378
|
+
path.join(packageRoot, 'test', 'conformance', 'ISO-COMPLIANCE.md'),
|
|
8379
|
+
]) {
|
|
8380
|
+
const relative = path.relative(packageRoot, file);
|
|
8381
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
8382
|
+
const claims = [
|
|
8383
|
+
...[...text.matchAll(/\b(\d+)-case[^\n|]*WG17/g)].map((match) => Number(match[1])),
|
|
8384
|
+
...[...text.matchAll(/WG17[^\n|]*?\b(\d+) executable/g)].map((match) => Number(match[1])),
|
|
8385
|
+
];
|
|
8386
|
+
if (claims.length === 0) issues.push(`${relative}: WG17 total not found`);
|
|
8387
|
+
for (const claim of claims) {
|
|
8388
|
+
if (claim !== wg17) issues.push(`${relative}: WG17 count ${claim} != ${wg17}`);
|
|
8389
|
+
}
|
|
8390
|
+
}
|
|
8180
8391
|
return issues.sort();
|
|
8181
8392
|
}
|
|
8182
8393
|
|
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)])`
|
|
@@ -9334,6 +9385,8 @@ make the observed question explicit.
|
|
|
9334
9385
|
| --- | --- |
|
|
9335
9386
|
| `-h`, `--help` | Show usage |
|
|
9336
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 |
|
|
9337
9390
|
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
9338
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 |
|
|
9339
9392
|
| `--portable` | Enforce the conservative EyeProlog/Trealla/Scryer interoperability profile |
|
|
@@ -9378,7 +9431,9 @@ Work in a fixed sequence:
|
|
|
9378
9431
|
|
|
9379
9432
|
1. predict the ground answers before running the program;
|
|
9380
9433
|
2. run without observation flags and compare stdout with that prediction;
|
|
9381
|
-
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;
|
|
9382
9437
|
4. add `--warnings` when portability or negative dependencies are the
|
|
9383
9438
|
question; use `--portable` when non-profile dependencies must fail CI;
|
|
9384
9439
|
5. add `--stats` only when comparing two executions of the same semantic case.
|
|
@@ -9388,6 +9443,8 @@ For example:
|
|
|
9388
9443
|
```sh
|
|
9389
9444
|
eyeprolog --goal 'ancestor(X, Y)' examples/ancestor.pl
|
|
9390
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
|
|
9391
9448
|
eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
|
|
9392
9449
|
eyeprolog --portable --goal 'sudoku9_solution(S)' examples/clpz-sudoku-9x9.pl
|
|
9393
9450
|
eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
|
package/tools/upgrade-wg17.mjs
CHANGED
|
@@ -11,6 +11,11 @@ const syntaxSource = 'https://www.complang.tuwien.ac.at/ulrich/iso-prolog/confor
|
|
|
11
11
|
const syntaxFixturePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-cases.json');
|
|
12
12
|
const syntaxCoveragePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-coverage.json');
|
|
13
13
|
const syntaxStatusPath = path.join(packageRoot, 'test', 'conformance', 'WG17-SYNTAX-STATUS.md');
|
|
14
|
+
const conformanceReportPath = path.join(packageRoot, 'conformance-report.md');
|
|
15
|
+
const countDocumentationPaths = [
|
|
16
|
+
path.join(packageRoot, 'test', 'conformance', 'README.md'),
|
|
17
|
+
path.join(packageRoot, 'test', 'conformance', 'ISO-COMPLIANCE.md'),
|
|
18
|
+
];
|
|
14
19
|
|
|
15
20
|
const namedEntities = new Map([
|
|
16
21
|
['amp', '&'], ['lt', '<'], ['gt', '>'], ['quot', '"'], ['apos', "'"],
|
|
@@ -318,6 +323,20 @@ function formatIdList(ids) {
|
|
|
318
323
|
return ids.length === 0 ? 'none' : ids.map((id) => `#${id}`).join(', ');
|
|
319
324
|
}
|
|
320
325
|
|
|
326
|
+
function updateDocumentedInventoryCount(previousCount, nextCount) {
|
|
327
|
+
if (previousCount === nextCount) return;
|
|
328
|
+
for (const filename of countDocumentationPaths) {
|
|
329
|
+
const original = fs.readFileSync(filename, 'utf8');
|
|
330
|
+
const updated = original
|
|
331
|
+
.replaceAll(`${previousCount}-case`, `${nextCount}-case`)
|
|
332
|
+
.replaceAll(`WG17 matrix has ${previousCount} executable`, `WG17 matrix has ${nextCount} executable`);
|
|
333
|
+
if (updated === original) {
|
|
334
|
+
throw new Error(`WG17 inventory count was not found in ${path.relative(packageRoot, filename)}`);
|
|
335
|
+
}
|
|
336
|
+
fs.writeFileSync(filename, updated);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
321
340
|
async function readSource(source) {
|
|
322
341
|
if (/^https?:\/\//i.test(source)) {
|
|
323
342
|
const response = await fetch(source, {
|
|
@@ -415,6 +434,13 @@ export async function upgradeWg17({ check = false, source = syntaxSource } = {})
|
|
|
415
434
|
// Generate status after the fixture/manifest are synchronized.
|
|
416
435
|
const { renderWg17SyntaxStatus } = await import('./report-wg17-syntax-coverage.mjs');
|
|
417
436
|
fs.writeFileSync(syntaxStatusPath, renderWg17SyntaxStatus());
|
|
437
|
+
updateDocumentedInventoryCount(previous.cases.length, fixture.cases.length);
|
|
438
|
+
|
|
439
|
+
// The public report executes the refreshed fixture, so keep its measured row
|
|
440
|
+
// synchronized as part of the same explicit upgrade operation.
|
|
441
|
+
const { buildConformanceReport, formatConformanceReport } = await import('../test/run-conformance-report.mjs');
|
|
442
|
+
const conformanceReport = buildConformanceReport();
|
|
443
|
+
fs.writeFileSync(conformanceReportPath, formatConformanceReport(conformanceReport));
|
|
418
444
|
|
|
419
445
|
const upstreamAssertions = fixture.cases
|
|
420
446
|
.filter((item) => item.outcome == null)
|
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
|
|