gdp-wired 0.2.0 → 0.4.0
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/README.md +128 -92
- package/dist/core/compose.d.ts +15 -1
- package/dist/core/compose.js +132 -8
- package/dist/core/dsl.d.ts +272 -17
- package/dist/core/dsl.js +1098 -60
- package/dist/core/machine.d.ts +34 -6
- package/dist/core/machine.js +194 -22
- package/dist/core/watch.d.ts +14 -0
- package/dist/core/watch.js +78 -0
- package/dist/kernel/kernel.d.ts +30 -2
- package/dist/kernel/kernel.js +306 -48
- package/dist/kernel/prove.d.ts +36 -9
- package/dist/kernel/prove.js +317 -28
- package/dist/lean-kernel/emit.d.ts +124 -0
- package/dist/lean-kernel/emit.js +875 -0
- package/dist/lean-kernel/lean-terms.d.ts +5 -0
- package/dist/lean-kernel/lean-terms.js +44 -0
- package/dist/lean-kernel/library.d.ts +1 -0
- package/dist/lean-kernel/library.js +13 -0
- package/package.json +11 -4
package/dist/kernel/prove.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { add, k, scale, solve, sub } from "../core/solver.js";
|
|
2
|
-
import { canon, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, obligations, stepLawObligations, truth, unreachable, withProducts } from "./kernel.js";
|
|
2
|
+
import { assumed, canon, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
|
|
3
3
|
// The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
|
|
4
4
|
function openE(e, path, out) {
|
|
5
5
|
switch (e.op) {
|
|
@@ -32,7 +32,7 @@ function openE(e, path, out) {
|
|
|
32
32
|
function openC(c, path, out) {
|
|
33
33
|
if (truth(c, path) !== undefined)
|
|
34
34
|
return;
|
|
35
|
-
if (c.op === 'flag')
|
|
35
|
+
if (c.op === 'flag' || c.op === 'all')
|
|
36
36
|
return;
|
|
37
37
|
if (c.op === 'not')
|
|
38
38
|
return openC(c.x, path, out);
|
|
@@ -68,7 +68,7 @@ function openH(c, path, out, positive = true) {
|
|
|
68
68
|
openC(c, path, out);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
if (c.op === 'flag')
|
|
71
|
+
if (c.op === 'flag' || c.op === 'all')
|
|
72
72
|
return;
|
|
73
73
|
const before = out.length;
|
|
74
74
|
openE(c.a, path, out), openE(c.b, path, out);
|
|
@@ -113,10 +113,11 @@ function parse(text) {
|
|
|
113
113
|
}
|
|
114
114
|
return out;
|
|
115
115
|
}
|
|
116
|
-
// The budget: 200,000 nodes and, by default, 20 seconds per goal; past either the goal
|
|
117
|
-
|
|
116
|
+
// The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
|
|
117
|
+
// is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
|
|
118
|
+
export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
|
|
118
119
|
if (++budget.nodes > 200_000 || (budget.until !== undefined && performance.now() > budget.until))
|
|
119
|
-
return { stuck: 'unknown', path };
|
|
120
|
+
return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
|
|
120
121
|
const rows = leafConstraints(goal, path);
|
|
121
122
|
const verdict = solve(rows);
|
|
122
123
|
if (verdict.kind === 'refuted')
|
|
@@ -181,6 +182,13 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
|
|
|
181
182
|
return no;
|
|
182
183
|
return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
|
|
183
184
|
}
|
|
185
|
+
// What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
|
|
186
|
+
function outOfBudget(goal, limit, depth) {
|
|
187
|
+
const steps = Number(/@(\d+) steps/.exec(goal.name)?.[1] ?? 1);
|
|
188
|
+
return `the prover's ${limit === 'nodes' ? '200,000-node' : 'time'} budget ran out at ${goal.hyps.length} hypotheses, ${depth} case splits deep. ` +
|
|
189
|
+
`Each case a step decides multiplies the cases to refute, so ${steps > 1 ? `a law over ${steps} steps` : 'many interacting cases'} can outgrow it: ` +
|
|
190
|
+
'state the law over fewer steps (a one-step law and an invariant often give the same), or split it into laws about fewer conditions';
|
|
191
|
+
}
|
|
184
192
|
// Integer completeness for bounded systems: when elimination is inexact and finds no witness, halve a variable's range
|
|
185
193
|
// (x <= mid or x >= mid + 1). Each half is a split like any other, so the kernel checks it the same way; a variable
|
|
186
194
|
// pinned to one value becomes an equality, which the solver substitutes exactly.
|
|
@@ -223,11 +231,17 @@ function fractional(point, path) {
|
|
|
223
231
|
return undefined;
|
|
224
232
|
}
|
|
225
233
|
// The keys a proof assumes the every-key laws at: every key the step and the laws read, and the fresh key of the goal.
|
|
234
|
+
// A key bound by a quantified flag or a write at every key isn't one: those reads are at every key already.
|
|
226
235
|
function readKeys(m, laws) {
|
|
227
236
|
const out = new Map();
|
|
237
|
+
const bound = new Set([...Object.values(m.step.flags ?? {}).flatMap(c => (c.op === 'all' ? [c.key] : [])), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.each] : []))]);
|
|
238
|
+
const free = (e) => !JSON.stringify(e).match(/"name":"([^"]+)"/g)?.some(n => bound.has(n.slice(8, -1)));
|
|
228
239
|
const inE = (e) => {
|
|
229
|
-
if (e.op === 'get')
|
|
230
|
-
(
|
|
240
|
+
if (e.op === 'get') {
|
|
241
|
+
if (free(e.key))
|
|
242
|
+
out.set(JSON.stringify(e.key), e.key);
|
|
243
|
+
inE(e.key);
|
|
244
|
+
}
|
|
231
245
|
else if (e.op === 'if')
|
|
232
246
|
(inC(e.c), inE(e.then), inE(e.else));
|
|
233
247
|
else if (e.op === 'mul')
|
|
@@ -235,26 +249,214 @@ function readKeys(m, laws) {
|
|
|
235
249
|
else if ('a' in e)
|
|
236
250
|
(inE(e.a), inE(e.b));
|
|
237
251
|
};
|
|
238
|
-
const inC = (c) => (c.op === 'flag' ? undefined : c.op === 'not' ? inC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(inC) : (inE(c.a), inE(c.b)));
|
|
252
|
+
const inC = (c) => (c.op === 'flag' ? undefined : c.op === 'all' ? inC(c.c) : c.op === 'not' ? inC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(inC) : (inE(c.a), inE(c.b)));
|
|
239
253
|
Object.values(m.step.state ?? {}).forEach(inE);
|
|
240
254
|
Object.values(m.step.flags ?? {}).forEach(inC);
|
|
241
|
-
for (const w of Object.values(m.step.maps ?? {}))
|
|
242
|
-
(
|
|
255
|
+
for (const w of Object.values(m.step.maps ?? {})) {
|
|
256
|
+
if (!('each' in w)) {
|
|
257
|
+
out.set(JSON.stringify(w.key), w.key);
|
|
258
|
+
inE(w.key);
|
|
259
|
+
}
|
|
260
|
+
inE(w.value);
|
|
261
|
+
}
|
|
243
262
|
for (const l of laws.filter(l => !l.forall))
|
|
244
263
|
inC(l.holds);
|
|
245
264
|
return [...out.values()];
|
|
246
265
|
}
|
|
266
|
+
const shifts = (m) => Object.values(m.step.maps ?? {}).some(w => 'each' in w && !plainKey(w.value, w.each));
|
|
267
|
+
// The keys some goals read a map at.
|
|
268
|
+
function mapKeys(goals) {
|
|
269
|
+
const out = new Map();
|
|
270
|
+
const inE = (e) => {
|
|
271
|
+
if (e.op === 'get') {
|
|
272
|
+
out.set(JSON.stringify(e.key), e.key);
|
|
273
|
+
inE(e.key);
|
|
274
|
+
}
|
|
275
|
+
else if (e.op === 'if')
|
|
276
|
+
(inC(e.c), inE(e.then), inE(e.else));
|
|
277
|
+
else if (e.op === 'mul')
|
|
278
|
+
inE(e.a);
|
|
279
|
+
else if ('a' in e)
|
|
280
|
+
(inE(e.a), inE(e.b));
|
|
281
|
+
};
|
|
282
|
+
const inC = (c) => (c.op === 'flag' ? undefined : c.op === 'all' ? inC(c.c) : c.op === 'not' ? inC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(inC) : (inE(c.a), inE(c.b)));
|
|
283
|
+
for (const g of goals)
|
|
284
|
+
g.hyps.forEach(inC);
|
|
285
|
+
return [...out.values()];
|
|
286
|
+
}
|
|
287
|
+
// Instances with each every-key law also at these keys. A law about two keys or more goes at tuples that relate the
|
|
288
|
+
// goal's own keys (`focus`: the skolems of the law it proves, or a step law's keys) to each other and to the keys that
|
|
289
|
+
// read none of them: at the i-th place, a key that reads the i-th focus alone (the first, past the focus there is), and
|
|
290
|
+
// at most one place a key that reads no focus.
|
|
291
|
+
const withReads = (laws, inst, keys, focus) => ({ ...inst, ...Object.fromEntries(laws.filter(l => l.forall).map(l => {
|
|
292
|
+
const had = inst[l.name] ?? [];
|
|
293
|
+
if (l.keys?.length) {
|
|
294
|
+
const n = l.keys.length + 1;
|
|
295
|
+
const reads = (e, x) => x !== undefined && JSON.stringify(e).includes(`"name":${JSON.stringify(x)}`);
|
|
296
|
+
const own = (i) => { const f = focus[i] ?? focus[0]; return keys.filter(e => reads(e, f) && focus.every(g => g === f || !reads(e, g))); };
|
|
297
|
+
const R = keys.filter(e => !focus.some(f => reads(e, f)));
|
|
298
|
+
const out = new Map();
|
|
299
|
+
for (let i = 0; i + n <= had.length; i += n)
|
|
300
|
+
out.set(JSON.stringify(had.slice(i, i + n)), had.slice(i, i + n));
|
|
301
|
+
const go = (i, tuple, rest) => {
|
|
302
|
+
if (i === n) {
|
|
303
|
+
out.set(JSON.stringify(tuple), tuple);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const e of own(i))
|
|
307
|
+
go(i + 1, [...tuple, e], rest);
|
|
308
|
+
if (!rest)
|
|
309
|
+
for (const e of R)
|
|
310
|
+
go(i + 1, [...tuple, e], true);
|
|
311
|
+
};
|
|
312
|
+
go(0, [], false);
|
|
313
|
+
return [l.name, [...out.values()].flat()];
|
|
314
|
+
}
|
|
315
|
+
const seen = new Set(had.map(e => JSON.stringify(e)));
|
|
316
|
+
return [l.name, [...had, ...keys.filter(e => { const id = JSON.stringify(e); return seen.has(id) ? false : (seen.add(id), true); })]];
|
|
317
|
+
})) });
|
|
318
|
+
// A law about every n keys at every n-tuple of the keys (flattened, as Instances keeps tuples).
|
|
319
|
+
const tuples = (keys, n) => (n === 0 ? [] : n === 1 ? keys : keys.flatMap(a => { const rest = tuples(keys, n - 1); return Array.from({ length: rest.length / (n - 1) }, (_, t) => [a, ...rest.slice(t * (n - 1), (t + 1) * (n - 1))]).flat(); }));
|
|
320
|
+
// The keys some goals read a function at.
|
|
321
|
+
function fnKeys(m, goals) {
|
|
322
|
+
const fns = Object.keys(m.fns ?? {}), out = new Map();
|
|
323
|
+
const inE = (e) => {
|
|
324
|
+
if (e.op === 'get') {
|
|
325
|
+
if (fns.includes(e.map))
|
|
326
|
+
out.set(JSON.stringify(e.key), e.key);
|
|
327
|
+
inE(e.key);
|
|
328
|
+
}
|
|
329
|
+
else if (e.op === 'if')
|
|
330
|
+
(inC(e.c), inE(e.then), inE(e.else));
|
|
331
|
+
else if (e.op === 'mul')
|
|
332
|
+
inE(e.a);
|
|
333
|
+
else if ('a' in e)
|
|
334
|
+
(inE(e.a), inE(e.b));
|
|
335
|
+
};
|
|
336
|
+
const inC = (c) => (c.op === 'flag' ? undefined : c.op === 'all' ? inC(c.c) : c.op === 'not' ? inC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(inC) : (inE(c.a), inE(c.b)));
|
|
337
|
+
for (const g of goals)
|
|
338
|
+
g.hyps.forEach(inC);
|
|
339
|
+
return [...out.values()];
|
|
340
|
+
}
|
|
341
|
+
// Instances with each fact about every key assumed where it reads a function at one of these keys: its key at each
|
|
342
|
+
// value that makes one of its reads one of these (by matching: a function of several arguments reads a paired key), and
|
|
343
|
+
// at the keys themselves.
|
|
344
|
+
const withFacts = (m, inst, keys) => ({ ...inst, ...Object.fromEntries((m.facts ?? []).filter(f => f.forall).map(f => {
|
|
345
|
+
if (f.keys?.length)
|
|
346
|
+
return [f.name, tuples(keys, f.keys.length + 1)];
|
|
347
|
+
const had = inst[f.name] ?? [], seen = new Set(had.map(e => JSON.stringify(e)));
|
|
348
|
+
const reads = fnKeys(m, [{ name: '', hyps: [f.holds] }]);
|
|
349
|
+
const at = [...keys, ...reads.flatMap(pattern => keys.flatMap(key => { const b = match(pattern, key, f.forall); return b ? [b] : []; }))];
|
|
350
|
+
return [f.name, [...had, ...at.filter(e => { const id = JSON.stringify(e); return seen.has(id) ? false : (seen.add(id), true); })]];
|
|
351
|
+
})) });
|
|
352
|
+
// The value of x that makes pattern the term, if one does (structurally).
|
|
353
|
+
function match(pattern, term, x) {
|
|
354
|
+
let bound;
|
|
355
|
+
const go = (p, t) => {
|
|
356
|
+
if (p.op === 'var' && p.name === x) {
|
|
357
|
+
if (bound)
|
|
358
|
+
return JSON.stringify(bound) === JSON.stringify(t);
|
|
359
|
+
bound = t;
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
if (p.op !== t.op)
|
|
363
|
+
return false;
|
|
364
|
+
const pe = Object.entries(p), te = t;
|
|
365
|
+
return pe.every(([k, v]) => k === 'op' || (typeof v === 'object' && v !== null ? (Array.isArray(v) ? Array.isArray(te[k]) && te[k].length === v.length && v.every((y, i) => go(y, te[k][i])) : go(v, te[k])) : v === te[k]));
|
|
366
|
+
};
|
|
367
|
+
return go(pattern, term) ? bound : undefined;
|
|
368
|
+
}
|
|
369
|
+
// Each quantified flag's witness w?f: a key where its condition fails, when it does; every-key facts are wanted there too.
|
|
370
|
+
const witnesses = (m) => Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [{ op: 'var', name: `w?${f}` }] : []));
|
|
371
|
+
// The keys each quantified flag is assumed at (instances under ?f).
|
|
372
|
+
const flagInstances = (m, keys) => Object.fromEntries(Object.entries(m.step.flags ?? {}).flatMap(([f, c]) => (c.op === 'all' ? [[`?${f}`, keys]] : [])));
|
|
373
|
+
const someKeys = (c, positive = true) => c.op === 'all' ? [...(positive ? [] : [c]), ...someKeys(c.c, positive)] : c.op === 'not' ? someKeys(c.x, !positive) : c.op === 'and' || c.op === 'or' ? c.xs.flatMap(y => someKeys(y, positive)) : [];
|
|
374
|
+
function atWitness(c, picks, positive = true) {
|
|
375
|
+
switch (c.op) {
|
|
376
|
+
case 'all': return !positive && picks[c.key] ? atWitness(substVar(c.c, c.key, picks[c.key]), picks, positive) : { ...c, c: atWitness(c.c, picks, positive) };
|
|
377
|
+
case 'not': return { op: 'not', x: atWitness(c.x, picks, !positive) };
|
|
378
|
+
case 'and':
|
|
379
|
+
case 'or': return { op: c.op, xs: c.xs.map(y => atWitness(y, picks, positive)) };
|
|
380
|
+
case 'flag':
|
|
381
|
+
case '<':
|
|
382
|
+
case '<=':
|
|
383
|
+
case '=': return c;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function substVar(c, name, by) {
|
|
387
|
+
const e = (t) => {
|
|
388
|
+
switch (t.op) {
|
|
389
|
+
case 'var': return t.name === name ? by : t;
|
|
390
|
+
case 'const': return t;
|
|
391
|
+
case 'mul': return { ...t, a: e(t.a) };
|
|
392
|
+
case 'if': return { op: 'if', c: substVar(t.c, name, by), then: e(t.then), else: e(t.else) };
|
|
393
|
+
case 'get': return { ...t, key: e(t.key) };
|
|
394
|
+
case 'add':
|
|
395
|
+
case 'sub':
|
|
396
|
+
case 'min':
|
|
397
|
+
case 'max':
|
|
398
|
+
case 'prod': return { ...t, a: e(t.a), b: e(t.b) };
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
return c.op === 'flag' ? c : c.op === 'all' ? { ...c, c: substVar(c.c, name, by) } : c.op === 'not' ? { op: 'not', x: substVar(c.x, name, by) } : c.op === 'and' || c.op === 'or' ? { op: c.op, xs: c.xs.map(y => substVar(y, name, by)) } : { ...c, a: e(c.a), b: e(c.b) };
|
|
402
|
+
}
|
|
403
|
+
const candidates = (m, names) => {
|
|
404
|
+
const vars = [...new Set([...names, ...m.params, ...Object.keys(m.state)])];
|
|
405
|
+
return [{ op: 'const', value: 0 }, ...vars.map((name) => ({ op: 'var', name })), ...Object.keys(m.maps ?? {}).flatMap(map => vars.map((name) => ({ op: 'get', map, key: { op: 'var', name } })))];
|
|
406
|
+
};
|
|
407
|
+
// Witnesses for every some-key in `items`, chosen in order: each the first candidate under which `proves` accepts the
|
|
408
|
+
// items whose some-keys are all named so far. Undefined, with the some-key's law, if none does.
|
|
409
|
+
function chooseWitnesses(items, body, names, m, proves, rebuild) {
|
|
410
|
+
const picks = {};
|
|
411
|
+
for (const t of items)
|
|
412
|
+
for (const q of someKeys(body(t)))
|
|
413
|
+
if (q.witness)
|
|
414
|
+
picks[q.key] = q.witness;
|
|
415
|
+
const resolved = (all) => items.filter(t => all || someKeys(body(t)).every(q => q.key in picks)).map(t => (someKeys(body(t)).length ? rebuild(t, atWitness(body(t), picks)) : t));
|
|
416
|
+
for (const t of items)
|
|
417
|
+
for (const q of someKeys(body(t))) {
|
|
418
|
+
if (q.key in picks)
|
|
419
|
+
continue;
|
|
420
|
+
const found = candidates(m, names(t)).find(w => { picks[q.key] = w; return proves(resolved(false)); });
|
|
421
|
+
if (!found) {
|
|
422
|
+
delete picks[q.key];
|
|
423
|
+
return { stuck: `no witness found for some key in ${t.name}: name one (someKey(f, witness))` };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { items: resolved(true) };
|
|
427
|
+
}
|
|
247
428
|
export function proveLaws(m, laws) {
|
|
248
|
-
|
|
429
|
+
if (!laws.some(l => someKeys(l.holds).length))
|
|
430
|
+
return proveLawsAsGiven(m, laws);
|
|
431
|
+
const out = chooseWitnesses(laws, l => l.holds, l => (l.forall ? [l.forall] : []), m, ls => { const r = proveLawsAsGiven(m, ls); return r.proven && r.checked; }, (l, holds) => ({ ...l, holds, stated: l.stated ?? l.holds }));
|
|
432
|
+
if ('stuck' in out)
|
|
433
|
+
return { proven: false, nodes: 0, leaves: 0, failed: { goal: 'witnesses', stuck: out.stuck }, checked: false };
|
|
434
|
+
return { ...proveLawsAsGiven(m, out.items), laws: out.items };
|
|
435
|
+
}
|
|
436
|
+
function proveLawsAsGiven(m, laws) {
|
|
437
|
+
const keys = [...readKeys(m, laws), ...witnesses(m)];
|
|
249
438
|
const instances = {};
|
|
250
|
-
for (const law of laws)
|
|
251
|
-
|
|
439
|
+
for (const law of laws) {
|
|
440
|
+
const at = [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))];
|
|
441
|
+
instances[law.name] = { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : at])), ...flagInstances(m, at) };
|
|
442
|
+
}
|
|
443
|
+
// The facts about functions, at every key each law's goals read a function at (found from the goals, twice round).
|
|
444
|
+
for (let round = 0; round < 2 && m.facts?.length; round++) {
|
|
445
|
+
const goals = obligations(m, laws, instances);
|
|
446
|
+
laws.forEach((law, i) => { instances[law.name] = withFacts(m, instances[law.name], fnKeys(m, goals.slice(2 * i, 2 * i + 2))); });
|
|
447
|
+
}
|
|
448
|
+
// Where positions shift (a write at every key reading its key at k ∸ 1 or k + 1) or a law is about two keys, the
|
|
449
|
+
// every-key laws also at each key the goals read a map at, once round.
|
|
450
|
+
if (shifts(m) || laws.some(l => l.keys?.length)) {
|
|
451
|
+
const goals = obligations(m, laws, instances);
|
|
452
|
+
laws.forEach((law, i) => { instances[law.name] = withReads(laws, instances[law.name], mapKeys(goals.slice(2 * i, 2 * i + 2)), [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])]); });
|
|
453
|
+
}
|
|
252
454
|
const trees = {};
|
|
253
455
|
let nodes = 0, leaves = 0;
|
|
254
456
|
for (const goal of obligations(m, laws, instances)) {
|
|
255
457
|
const out = grow(goal);
|
|
256
458
|
if (!('tree' in out))
|
|
257
|
-
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) }, checked: false };
|
|
459
|
+
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
258
460
|
trees[goal.name] = out.tree;
|
|
259
461
|
nodes += out.nodes;
|
|
260
462
|
leaves += countLeaves(out.tree);
|
|
@@ -262,29 +464,93 @@ export function proveLaws(m, laws) {
|
|
|
262
464
|
const proof = { instances, trees };
|
|
263
465
|
return { proven: true, nodes, leaves, checked: checkLaws(m, laws, proof), proof };
|
|
264
466
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
export function proveStepLaws(m, laws, stepLaws) {
|
|
268
|
-
const base = proveLaws(m, laws);
|
|
467
|
+
export function proveStepLaws(m, given, stepLaws) {
|
|
468
|
+
const base = proveLaws(m, given);
|
|
269
469
|
if (!base.proven || !base.proof)
|
|
270
470
|
return { proven: false, nodes: base.nodes, leaves: base.leaves, ...(base.failed ? { failed: base.failed } : {}), checked: false };
|
|
471
|
+
const laws = base.laws ?? given;
|
|
472
|
+
if (stepLaws.some(sl => someKeys(sl.then).length)) {
|
|
473
|
+
// Each step law's witnesses on its own (a step law assumes no other), the request and the state after among them.
|
|
474
|
+
const plain = m.history ? historyMachine(m).machine : m, after = Object.keys(plain.state).map(n => `${n}'`);
|
|
475
|
+
const out = chooseWitnesses(stepLaws, sl => sl.then, sl => [...(sl.forall ? [sl.forall] : []), ...plain.inputs, ...after], plain, sls => sls.every(sl => { const r = proveStepLawsAsGiven(m, laws, [sl], base); return r.proven && r.checked; }), (sl, then) => ({ ...sl, then }));
|
|
476
|
+
if ('stuck' in out)
|
|
477
|
+
return { proven: false, nodes: base.nodes, leaves: base.leaves, failed: { goal: 'witnesses', stuck: out.stuck }, checked: false };
|
|
478
|
+
return { ...proveStepLawsAsGiven(m, laws, out.items, base), ...(base.laws ? { laws } : {}), stepLaws: out.items };
|
|
479
|
+
}
|
|
480
|
+
return { ...proveStepLawsAsGiven(m, laws, stepLaws, base), ...(base.laws ? { laws } : {}) };
|
|
481
|
+
}
|
|
482
|
+
function proveStepLawsAsGiven(m, laws, stepLaws, base) {
|
|
483
|
+
if (!base.proof)
|
|
484
|
+
return { proven: false, nodes: base.nodes, leaves: base.leaves, checked: false };
|
|
271
485
|
const plain = m.history ? historyMachine(m).machine : m;
|
|
272
|
-
const keys = [...readKeys(plain, laws), ...stepLaws.flatMap(sl => [sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC))
|
|
273
|
-
|
|
486
|
+
const keys = [...readKeys(plain, laws), ...witnesses(plain), ...stepLaws.flatMap(sl => [sl.then, ...(sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when])].flatMap(readKeysC)),
|
|
487
|
+
...stepLaws.flatMap(sl => [sl.forall, ...(sl.keys ?? [])].flatMap(key => (key ? [{ op: 'var', name: key }] : [])))];
|
|
488
|
+
let instances = { ...Object.fromEntries(laws.filter(l => l.forall).map(l => [l.name, l.keys?.length ? [] : keys])), ...flagInstances(plain, keys) };
|
|
489
|
+
for (let round = 0; round < 2 && plain.facts?.length; round++)
|
|
490
|
+
instances = withFacts(plain, instances, fnKeys(plain, stepLaws.flatMap(sl => stepLawObligations(m, laws, instances, sl))));
|
|
491
|
+
// Where positions shift or a law is about two keys, each step law gets its own instances, from its own goal.
|
|
492
|
+
const perStep = {};
|
|
493
|
+
if (shifts(plain) || laws.some(l => l.keys?.length))
|
|
494
|
+
for (const sl of stepLaws)
|
|
495
|
+
perStep[sl.name] = withReads(laws, instances, mapKeys(stepLawObligations(m, laws, instances, sl)), [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])));
|
|
274
496
|
const trees = {};
|
|
275
497
|
let nodes = base.nodes, leaves = base.leaves;
|
|
276
498
|
for (const sl of stepLaws)
|
|
277
|
-
for (const goal of stepLawObligations(m, laws, instances, sl)) {
|
|
499
|
+
for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
|
|
278
500
|
const out = grow(goal);
|
|
279
501
|
if (!('tree' in out))
|
|
280
|
-
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}) }, checked: false };
|
|
502
|
+
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
281
503
|
trees[goal.name] = out.tree;
|
|
282
504
|
nodes += out.nodes;
|
|
283
505
|
leaves += countLeaves(out.tree);
|
|
284
506
|
}
|
|
285
|
-
const proof = { laws: base.proof, instances, trees };
|
|
507
|
+
const proof = { laws: base.proof, instances, ...(Object.keys(perStep).length ? { perStep } : {}), trees };
|
|
286
508
|
return { proven: true, nodes, leaves, checked: checkStepLaws(m, laws, stepLaws, proof), proof };
|
|
287
509
|
}
|
|
510
|
+
// Progress (kernel.ts's progressObligations): the laws proven first, then each goal, the every-key laws assumed at every
|
|
511
|
+
// key the goals read a map at.
|
|
512
|
+
export function proveProgress(m, laws, p) {
|
|
513
|
+
const base = proveLaws(m, laws);
|
|
514
|
+
if (!base.proven || !base.checked)
|
|
515
|
+
return { proven: false, nodes: base.nodes, leaves: base.leaves, ...(base.failed ? { failed: base.failed } : {}), checked: false };
|
|
516
|
+
const ls = base.laws ?? laws;
|
|
517
|
+
let instances = { ...withReads(ls, {}, mapKeys(progressObligations(m, ls, {}, p)), []), ...flagInstances(m, mapKeys(progressObligations(m, ls, {}, p))) };
|
|
518
|
+
instances = { ...instances, ...withReads(ls, instances, mapKeys(progressObligations(m, ls, instances, p)), []) };
|
|
519
|
+
if (m.facts?.length)
|
|
520
|
+
instances = withFacts(m, instances, fnKeys(m, progressObligations(m, ls, instances, p)));
|
|
521
|
+
const trees = {};
|
|
522
|
+
let nodes = base.nodes, leaves = base.leaves;
|
|
523
|
+
for (const goal of progressObligations(m, ls, instances, p)) {
|
|
524
|
+
const out = grow(goal);
|
|
525
|
+
if (!('tree' in out))
|
|
526
|
+
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
527
|
+
trees[goal.name] = out.tree;
|
|
528
|
+
nodes += out.nodes;
|
|
529
|
+
leaves += countLeaves(out.tree);
|
|
530
|
+
}
|
|
531
|
+
return { proven: true, nodes, leaves, checked: progressObligations(m, ls, instances, p).every(g => check(g, trees[g.name])), instances, trees };
|
|
532
|
+
}
|
|
533
|
+
// Liveness (kernel.ts's livenessParts): its step laws, proven together. The claim needs no more (a run takes the
|
|
534
|
+
// request, so it is allowed); but a request never allowed would leave the claim about no run, so where a helpful request
|
|
535
|
+
// names every input a client sends and carries no maps, it is proven allowed in its region too.
|
|
536
|
+
export function proveLiveness(m, laws, l) {
|
|
537
|
+
const { stay, cover, helps } = livenessParts(m, l);
|
|
538
|
+
const a = proveStepLaws(m, laws, [stay, cover, ...helps]);
|
|
539
|
+
if (!a.proven || !a.checked)
|
|
540
|
+
return { proven: false, nodes: a.nodes, leaves: a.leaves, ...(a.failed ? { failed: a.failed } : {}), checked: false };
|
|
541
|
+
const plain = m.history ? historyMachine(m).machine : m;
|
|
542
|
+
let nodes = a.nodes, leaves = a.leaves;
|
|
543
|
+
for (const [i, h] of livenessHelpful(l).entries()) {
|
|
544
|
+
if (m.history || plain.inputMaps?.length || !plain.assume || !plain.inputs.every(inp => inp in h.request))
|
|
545
|
+
continue;
|
|
546
|
+
const b = proveProgress(plain, laws, { name: `${l.name}: helpful request ${i + 1} is allowed`, when: { op: 'and', xs: [l.from, { op: 'not', x: l.to }, h.when] }, then: trueC, request: h.request });
|
|
547
|
+
nodes += b.nodes;
|
|
548
|
+
leaves += b.leaves;
|
|
549
|
+
if (!b.proven || !b.checked)
|
|
550
|
+
return { proven: false, nodes, leaves, ...(b.failed ? { failed: b.failed } : {}), checked: false };
|
|
551
|
+
}
|
|
552
|
+
return { proven: true, checked: true, nodes, leaves, steps: a };
|
|
553
|
+
}
|
|
288
554
|
// The key terms a condition reads maps at; a primed map's key is a key of the map before the step too.
|
|
289
555
|
function readKeysC(c) {
|
|
290
556
|
const out = [];
|
|
@@ -296,14 +562,14 @@ function readKeysC(c) {
|
|
|
296
562
|
inE(e.a);
|
|
297
563
|
else if ('a' in e)
|
|
298
564
|
(inE(e.a), inE(e.b)); };
|
|
299
|
-
const inC = (x) => (x.op === 'flag' ? undefined : x.op === 'not' ? inC(x.x) : x.op === 'and' || x.op === 'or' ? x.xs.forEach(inC) : (inE(x.a), inE(x.b)));
|
|
565
|
+
const inC = (x) => (x.op === 'flag' || x.op === 'all' ? undefined : x.op === 'not' ? inC(x.x) : x.op === 'and' || x.op === 'or' ? x.xs.forEach(inC) : (inE(x.a), inE(x.b)));
|
|
300
566
|
inC(c);
|
|
301
567
|
return out;
|
|
302
568
|
}
|
|
303
569
|
export const countLeaves = (t) => ('leaf' in t ? 1 : countLeaves(t.yes) + countLeaves(t.no));
|
|
304
570
|
// A real counterexample for a goal the prover couldn't close (with products, its witness may be abstract): the free
|
|
305
571
|
// variables over 0..bound, every hypothesis evaluated with true arithmetic. Goals that read maps are left to the
|
|
306
|
-
// machine searches (search/), which run whole histories.
|
|
572
|
+
// machine searches (research/search/), which run whole histories.
|
|
307
573
|
export function concretize(goal, bound = 12) {
|
|
308
574
|
const names = new Set();
|
|
309
575
|
let reads = false;
|
|
@@ -315,7 +581,7 @@ export function concretize(goal, bound = 12) {
|
|
|
315
581
|
(walkC(e.c), walkE(e.then), walkE(e.else));
|
|
316
582
|
else if ('a' in e)
|
|
317
583
|
(walkE(e.a), 'b' in e && walkE(e.b)); };
|
|
318
|
-
const walkC = (c) => (c.op === 'flag' ? undefined : c.op === 'not' ? walkC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(walkC) : (walkE(c.a), walkE(c.b)));
|
|
584
|
+
const walkC = (c) => (c.op === 'flag' ? undefined : c.op === 'all' ? void (reads = true) : c.op === 'not' ? walkC(c.x) : c.op === 'and' || c.op === 'or' ? c.xs.forEach(walkC) : (walkE(c.a), walkE(c.b)));
|
|
319
585
|
goal.hyps.forEach(walkC);
|
|
320
586
|
if (reads || names.size > 4)
|
|
321
587
|
return undefined;
|
|
@@ -334,7 +600,7 @@ export function concretize(goal, bound = 12) {
|
|
|
334
600
|
}
|
|
335
601
|
};
|
|
336
602
|
const holds = (c, env) => c.op === 'not' ? !holds(c.x, env) : c.op === 'and' ? c.xs.every(y => holds(y, env)) : c.op === 'or' ? c.xs.some(y => holds(y, env))
|
|
337
|
-
: c.op === 'flag' ? false : c.op === '<' ? ev(c.a, env) < ev(c.b, env) : c.op === '<=' ? ev(c.a, env) <= ev(c.b, env) : ev(c.a, env) === ev(c.b, env);
|
|
603
|
+
: c.op === 'flag' || c.op === 'all' ? false : c.op === '<' ? ev(c.a, env) < ev(c.b, env) : c.op === '<=' ? ev(c.a, env) <= ev(c.b, env) : ev(c.a, env) === ev(c.b, env);
|
|
338
604
|
const vars = [...names];
|
|
339
605
|
const total = (bound + 1) ** vars.length;
|
|
340
606
|
for (let i = 0; i < total; i++) {
|
|
@@ -345,3 +611,26 @@ export function concretize(goal, bound = 12) {
|
|
|
345
611
|
}
|
|
346
612
|
return undefined;
|
|
347
613
|
}
|
|
614
|
+
// The facts a goal needs, as hypotheses: each fact about every key at the keys the goal reads a function at (and at
|
|
615
|
+
// the keys those facts read, a second round), each plain fact as it is.
|
|
616
|
+
export function factsFor(m, goal) {
|
|
617
|
+
let instances = {};
|
|
618
|
+
for (let round = 0; round < 2; round++)
|
|
619
|
+
instances = withFacts(m, instances, fnKeys(m, [{ ...goal, hyps: [...assumed(m.facts ?? [], instances), ...goal.hyps] }]));
|
|
620
|
+
return assumed(m.facts ?? [], instances);
|
|
621
|
+
}
|
|
622
|
+
// A goal built from its instances (an exporter's goal, shaped as its Lean twin builds it): the every-key laws, the
|
|
623
|
+
// quantified flags and the facts assumed at every key it reads a map or function at, then a tree, checked.
|
|
624
|
+
export function proveBuilt(m, laws, build) {
|
|
625
|
+
let instances = {};
|
|
626
|
+
for (let round = 0; round < 2; round++) {
|
|
627
|
+
const g = build(instances), keys = mapKeys([g]);
|
|
628
|
+
instances = { ...withReads(laws, instances, keys, []), ...flagInstances(m, keys) };
|
|
629
|
+
if (m.facts?.length)
|
|
630
|
+
instances = withFacts(m, instances, fnKeys(m, [build(instances)]));
|
|
631
|
+
}
|
|
632
|
+
const goal = build(instances), out = grow(goal);
|
|
633
|
+
if (!('tree' in out) || !check(goal, out.tree))
|
|
634
|
+
return { proven: false };
|
|
635
|
+
return { proven: true, instances, goal, tree: out.tree };
|
|
636
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { C, E, Law, Machine } from '../core/machine.js';
|
|
2
|
+
import { type Step } from '../core/solver.js';
|
|
3
|
+
import { type Fold, type Goal, type Instances, type Liveness, type Loop, type Path, type StepLaw, type Tree } from '../kernel/kernel.js';
|
|
4
|
+
import { leanLibrary } from './library.js';
|
|
5
|
+
export { leanLibrary };
|
|
6
|
+
export type Exported = {
|
|
7
|
+
name: string;
|
|
8
|
+
goal: Goal;
|
|
9
|
+
tree: Tree;
|
|
10
|
+
};
|
|
11
|
+
export declare const int: (v: number) => string;
|
|
12
|
+
export declare const list: (xs: string[]) => string;
|
|
13
|
+
export declare function leaves(t: Tree, path: Path, visit: (t: Tree & {
|
|
14
|
+
leaf: Step[];
|
|
15
|
+
products?: [number, number][];
|
|
16
|
+
}, path: Path) => void): void;
|
|
17
|
+
export declare function encode(ex: Exported, tamper?: boolean, dropStep?: boolean, globalVars?: string[], globalMaps?: string[]): {
|
|
18
|
+
tab: string;
|
|
19
|
+
hyps: string;
|
|
20
|
+
tree: string;
|
|
21
|
+
cols: string[];
|
|
22
|
+
c: (t: C) => string;
|
|
23
|
+
e: (t: E) => string;
|
|
24
|
+
};
|
|
25
|
+
export declare function goalToLean(ex: Exported, i: number, tamper?: boolean, dropStep?: boolean): string;
|
|
26
|
+
export declare function machineToLean(id: string, m: Machine, laws: Law[], corollary?: string): string;
|
|
27
|
+
export declare const TRUE = ".le (.const 0) (.const 0)";
|
|
28
|
+
export declare const grown: (g: Goal) => Tree;
|
|
29
|
+
export declare function loopToLean(id: string, l: Loop, laws: Law[], rank: E | E[], tamper?: {
|
|
30
|
+
what: string;
|
|
31
|
+
loop: Loop;
|
|
32
|
+
}): string;
|
|
33
|
+
export declare function foldToLean(id: string, f: Fold, laws: Law[], extra?: string): string;
|
|
34
|
+
export declare function readable(id: string, laws: Law[], vars: string[], maps: string[], reach: {
|
|
35
|
+
binders: string;
|
|
36
|
+
hyp: string;
|
|
37
|
+
call: string;
|
|
38
|
+
}): string;
|
|
39
|
+
export type ProgressCase = {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
when: C;
|
|
43
|
+
then: C;
|
|
44
|
+
request: Record<string, E>;
|
|
45
|
+
instances: Instances;
|
|
46
|
+
tamper?: {
|
|
47
|
+
what: string;
|
|
48
|
+
when: C;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function mmachineToLean(id: string, m: Machine, laws: Law[], opts?: {
|
|
52
|
+
corollary?: string;
|
|
53
|
+
tamper?: {
|
|
54
|
+
what: string;
|
|
55
|
+
machine: Machine;
|
|
56
|
+
};
|
|
57
|
+
progress?: ProgressCase[];
|
|
58
|
+
free?: string[];
|
|
59
|
+
stepLaws?: StepLaw[];
|
|
60
|
+
}): string;
|
|
61
|
+
export declare const hasSets: (m: Machine) => boolean;
|
|
62
|
+
export declare function smachineToLean(id: string, m: Machine, laws: Law[], opts?: {
|
|
63
|
+
stepLaws?: StepLaw[];
|
|
64
|
+
liveness?: Liveness[];
|
|
65
|
+
tamper?: {
|
|
66
|
+
what: string;
|
|
67
|
+
machine: Machine;
|
|
68
|
+
};
|
|
69
|
+
simp?: string[];
|
|
70
|
+
free?: string[];
|
|
71
|
+
ghosts?: string[];
|
|
72
|
+
after?: (ix: (name: string) => number) => string;
|
|
73
|
+
refined?: {
|
|
74
|
+
impl: Machine;
|
|
75
|
+
spec: Machine;
|
|
76
|
+
wit: Record<string, E>;
|
|
77
|
+
stutter: C;
|
|
78
|
+
specPre: number;
|
|
79
|
+
};
|
|
80
|
+
}): string;
|
|
81
|
+
export declare function hmachineToLean(id: string, pruned: Machine, laws: Law[], tampers?: {
|
|
82
|
+
what: string;
|
|
83
|
+
relations: {
|
|
84
|
+
when: C;
|
|
85
|
+
holds: C;
|
|
86
|
+
}[];
|
|
87
|
+
withRecordTrees: boolean;
|
|
88
|
+
}[], corollary?: (ix: (name: string) => number) => string): string;
|
|
89
|
+
export type LeanItem = {
|
|
90
|
+
id: string;
|
|
91
|
+
machine: Machine;
|
|
92
|
+
laws: Law[];
|
|
93
|
+
stepLaws?: StepLaw[];
|
|
94
|
+
liveness?: Liveness[];
|
|
95
|
+
ghosts?: string[];
|
|
96
|
+
lean?: {
|
|
97
|
+
defs?: string;
|
|
98
|
+
simp?: string[];
|
|
99
|
+
after?: (ix: (name: string) => number) => string;
|
|
100
|
+
};
|
|
101
|
+
} | {
|
|
102
|
+
id: string;
|
|
103
|
+
refinement: {
|
|
104
|
+
impl: Machine;
|
|
105
|
+
spec: Machine;
|
|
106
|
+
relation: Law[];
|
|
107
|
+
witness: Record<string, E>;
|
|
108
|
+
stutter?: C;
|
|
109
|
+
};
|
|
110
|
+
} | {
|
|
111
|
+
id: string;
|
|
112
|
+
loop: Loop;
|
|
113
|
+
rank: E | E[];
|
|
114
|
+
laws?: Law[];
|
|
115
|
+
} | {
|
|
116
|
+
id: string;
|
|
117
|
+
goal: Goal;
|
|
118
|
+
};
|
|
119
|
+
export declare function toLean(items: LeanItem[], opts?: {
|
|
120
|
+
title?: string;
|
|
121
|
+
}): string;
|
|
122
|
+
export declare function leanProject(file: string, opts?: {
|
|
123
|
+
name?: string;
|
|
124
|
+
}): Record<string, string>;
|