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/conformance-report.md
CHANGED
|
@@ -8,9 +8,9 @@ when this report is generated; it is not inferred from fixture counts.
|
|
|
8
8
|
|
|
9
9
|
| Gate | Passed | Total | Status |
|
|
10
10
|
|---|---:|---:|---|
|
|
11
|
-
| WG17 syntax |
|
|
11
|
+
| WG17 syntax | 366 | 366 | pass |
|
|
12
12
|
|
|
13
|
-
The WG17 syntax row executes the vendored
|
|
13
|
+
The WG17 syntax row executes the vendored 366-case conformity-testing matrix
|
|
14
14
|
against EyeProlog's strict ISO reader/writer. A behavior fix such as operator-token
|
|
15
15
|
spelling therefore changes this report even when no corpus file is added or removed.
|
|
16
16
|
|
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
|
}
|