gdp-wired 0.15.0 → 0.17.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 +12 -0
- package/dist/core/chart.js +4 -4
- package/dist/core/cyclic.d.ts +51 -0
- package/dist/core/cyclic.js +295 -0
- package/dist/core/discover.d.ts +1 -1
- package/dist/core/discover.js +10 -5
- package/dist/core/dsl.d.ts +49 -4
- package/dist/core/dsl.js +143 -17
- package/dist/core/infer.js +123 -11
- package/dist/kernel/cyclic.d.ts +45 -0
- package/dist/kernel/cyclic.js +54 -0
- package/dist/kernel/prove.d.ts +10 -0
- package/dist/kernel/prove.js +348 -2
- package/dist/lean-kernel/emit.d.ts +13 -0
- package/dist/lean-kernel/emit.js +50 -2
- package/dist/lean-kernel/library.js +1 -0
- package/package.json +6 -2
package/dist/kernel/prove.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { add, k, refuteFast, scale, solve, sub, v } from "../core/solver.js";
|
|
2
|
-
import {
|
|
2
|
+
import { crossCyclic, cyclicMachine, cyclicOn, machineNodes, proveCyclic } from "../core/cyclic.js";
|
|
3
|
+
import { assumed, atomOf, boundOf, canon, canonFlip, columnTerm, decided, factorsOf, isCell, isMonomial, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, leafConstraintsReusing, reusing, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, witnessesOf, withProducts } from "./kernel.js";
|
|
3
4
|
// The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
|
|
4
5
|
function openE(e, path, out) {
|
|
5
6
|
switch (e.op) {
|
|
@@ -138,10 +139,45 @@ const solveP = (rows, maxRows) => {
|
|
|
138
139
|
return out;
|
|
139
140
|
};
|
|
140
141
|
// PROVE_TRACE=1 logs each goal grown from its root (hypotheses, nodes, time, outcome) to stderr.
|
|
142
|
+
// PROVE_DUMP: a hook research/perf/prof sets to read each root goal.
|
|
143
|
+
export let proveDump;
|
|
144
|
+
export const setProveDump = (f) => { proveDump = f; };
|
|
141
145
|
const proveTrace = !!globalThis.process?.env?.PROVE_TRACE;
|
|
146
|
+
// `why`, when the budget ran out: which limit, at what size, and what usually helps.
|
|
147
|
+
// Set while core/cyclic.ts grows its goals.
|
|
148
|
+
export const cellProducts = { on: false };
|
|
142
149
|
// The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
|
|
143
150
|
// is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
|
|
151
|
+
// A deadline every goal grown inside `withDeadline` keeps, beside its own budget: a search that proves many things (core/
|
|
152
|
+
// infer.ts, core/discover.ts, the search proveRule falls back on) bounds them all; a goal past it is left `unknown`.
|
|
153
|
+
let deadline = Infinity;
|
|
154
|
+
export function withDeadline(until, f) {
|
|
155
|
+
const outer = deadline;
|
|
156
|
+
deadline = Math.min(outer, until);
|
|
157
|
+
try {
|
|
158
|
+
return f();
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
deadline = outer;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// `units: false`: the goal's own order, no units first (a caller after a model, not a proof: a run guided by the
|
|
165
|
+
// prover's witnesses, say, whose models units would steer).
|
|
144
166
|
export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
|
|
167
|
+
if (deadline < (budget.until ?? Infinity))
|
|
168
|
+
budget.until = deadline;
|
|
169
|
+
if (budget.units === false && unitsNow) {
|
|
170
|
+
unitsNow = false;
|
|
171
|
+
try {
|
|
172
|
+
return grow(goal, path, budget);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
unitsNow = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (proveDump && !path.size && !budget.nodes) {
|
|
179
|
+
proveDump(goal);
|
|
180
|
+
}
|
|
145
181
|
if (proveTrace && !path.size && !budget.nodes) {
|
|
146
182
|
const t = performance.now(), out = growRoot(goal, path, budget);
|
|
147
183
|
console.error(`[grow] ${goal.name}: ${goal.hyps.length} hyps, ${budget.nodes} nodes, ${Math.round(performance.now() - t)} ms, ${'tree' in out ? 'proven' : `${out.stuck}${out.why ? ` (${out.why})` : ''}`}`);
|
|
@@ -157,6 +193,18 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
|
|
|
157
193
|
function growRoot(goal, path, budget) {
|
|
158
194
|
// At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
|
|
159
195
|
// two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
|
|
196
|
+
if (path.size === 0 && budget.nodes === 0 && unitFirst && unitsNow) {
|
|
197
|
+
// A goal that a few cases settle needs no units (finding them costs a Farkas check per atom); the rest get them.
|
|
198
|
+
unitsNow = false;
|
|
199
|
+
try {
|
|
200
|
+
const quick = growPlain(goal, path, { nodes: 0, until: budget.until, cap: 64 });
|
|
201
|
+
if ('tree' in quick || quick.stuck === 'sat' || quick.why === undefined)
|
|
202
|
+
return quick;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
unitsNow = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
160
208
|
if (path.size === 0 && budget.nodes === 0) {
|
|
161
209
|
const open = new Set(leafConstraints(goal, path).flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦') && columnTerm(n)?.op === 'if')).size;
|
|
162
210
|
const plain = growPlain(goal, path, { nodes: 0, until: budget.until, cap: Math.max(32, Math.round(4_000 / 2 ** (open / 2))) });
|
|
@@ -196,6 +244,8 @@ function growPlain(goal, path, budget) {
|
|
|
196
244
|
byHyp.set(h, (seen = {})); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
|
|
197
245
|
// Then the hypotheses about only the names the conclusion reads (an assumption about another key, a witness, can
|
|
198
246
|
// wait: a refutation seldom needs it), then the rest, each group in the goal's order.
|
|
247
|
+
if (unitFirst && unitsNow)
|
|
248
|
+
open.push(...unitsOf(goal));
|
|
199
249
|
if (last >= 0)
|
|
200
250
|
open.push(...openOf(hyps[last]));
|
|
201
251
|
const near = nearOf(goal);
|
|
@@ -237,6 +287,30 @@ function growPlain(goal, path, budget) {
|
|
|
237
287
|
if (real)
|
|
238
288
|
return { stuck: 'sat', witness: w, path };
|
|
239
289
|
}
|
|
290
|
+
// An equation about a factor (x' = x + 1, a run's next state as a cell) times each factor's sign: e·f = 0 for every
|
|
291
|
+
// factor f, so a monomial of x' is one of x (x'·x' = (x + 1)²). Whatever the number of rows: there are few such pairs.
|
|
292
|
+
// Only for cyclic proofs (core/cyclic.ts, whose runs keep each next state as a cell): the usual rules substitute it.
|
|
293
|
+
if (!split && monomials && cellProducts.on) {
|
|
294
|
+
const factorSet = new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)));
|
|
295
|
+
const byCanon = new Map(rows.map((r, i) => [canon(r.lin), i]));
|
|
296
|
+
const eqs = rows.flatMap((r, i) => (Object.keys(r.lin).some(key => key !== '1' && factorSet.has(key)) && byCanon.has(canon(scale(r.lin, -1))) ? [i] : []));
|
|
297
|
+
const signs = rows.flatMap((r, i) => { const keys = Object.keys(r.lin); return keys.length === 1 && factorSet.has(keys[0]) && r.lin[keys[0]] < 0 ? [i] : []; });
|
|
298
|
+
const pairs = eqs.flatMap(i => signs.map(j => [i, j])).slice(0, 200);
|
|
299
|
+
if (pairs.length) {
|
|
300
|
+
const sub = solve(withProducts(rows, pairs));
|
|
301
|
+
if (sub.kind === 'refuted')
|
|
302
|
+
return { tree: leafOf(sub.steps, pairs), nodes: budget.nodes };
|
|
303
|
+
// Then every linear row about a factor (n >= x: (n − x)·d >= 0) times each sign, past the forty rows the pairs
|
|
304
|
+
// below are kept to.
|
|
305
|
+
const about = rows.flatMap((r, i) => (!eqs.includes(i) && Object.keys(r.lin).some(key => key !== '1' && factorSet.has(key)) && !Object.keys(r.lin).some(isMonomial) ? [i] : []));
|
|
306
|
+
const more = [...pairs, ...about.flatMap(i => signs.map(j => [i, j]))].slice(0, 400);
|
|
307
|
+
if (more.length > pairs.length) {
|
|
308
|
+
const ineq = solve(withProducts(rows, more));
|
|
309
|
+
if (ineq.kind === 'refuted')
|
|
310
|
+
return { tree: leafOf(ineq.steps, more), nodes: budget.nodes };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
240
314
|
if (!split && monomials && rows.length <= 40) {
|
|
241
315
|
// Only rows about the factors (no other map cells), each pair once: elimination over hundreds of products blows up.
|
|
242
316
|
const factorSet = new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)));
|
|
@@ -283,6 +357,27 @@ function growPlain(goal, path, budget) {
|
|
|
283
357
|
return { tree: leafOf(third.steps, weighted, squares), nodes: budget.nodes };
|
|
284
358
|
}
|
|
285
359
|
}
|
|
360
|
+
// A multiplier: every row times a factor the path has at least 1 (the row 1 − x <= 0), with the squares (x·a − b)
|
|
361
|
+
// and (x·b − a) of each product a·b the rows read: a certificate that multiplies the goal by x first ((Σv)² <= n·Σv²
|
|
362
|
+
// over running samples is n times the goal, closed by (n·v − s)²).
|
|
363
|
+
const ones = rows.flatMap((r, i) => { const ks = Object.keys(r.lin); return ks.length === 2 && r.lin['1'] === 1 && ks.some(key => key !== '1' && r.lin[key] === -1 && factorSet.has(key)) ? [i] : []; });
|
|
364
|
+
for (const xi of ones.slice(0, 3)) {
|
|
365
|
+
const x = Object.keys(rows[xi].lin).find(key => key !== '1'), name = (fs) => [...fs].sort().join('*');
|
|
366
|
+
const forms = new Map();
|
|
367
|
+
for (const m of new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial)))) {
|
|
368
|
+
const [a, b, ...more] = factorsOf(m);
|
|
369
|
+
if (!a || !b || more.length)
|
|
370
|
+
continue;
|
|
371
|
+
for (const [u, w] of [[a, b], [b, a]]) {
|
|
372
|
+
const t = { [name([x, u])]: 1, [w]: -1 };
|
|
373
|
+
forms.set(canon(t), t);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const times = relevant.map(i => [i, xi]), sq = [...forms.values()].slice(0, 32);
|
|
377
|
+
const multiplied = solve(withProducts(rows, times, sq));
|
|
378
|
+
if (multiplied.kind === 'refuted')
|
|
379
|
+
return { tree: leafOf(multiplied.steps, times, sq), nodes: budget.nodes };
|
|
380
|
+
}
|
|
286
381
|
const factors = [...new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)))];
|
|
287
382
|
const fresh = (lin) => !path.has(canon(lin)) && !path.has(canonFlip(lin));
|
|
288
383
|
// Split a factor at zero, else two factors against each other (so (x - y)² >= 0 becomes a product of rows).
|
|
@@ -456,6 +551,51 @@ function nearOf(goal) {
|
|
|
456
551
|
}
|
|
457
552
|
return out;
|
|
458
553
|
}
|
|
554
|
+
// Units first: an atom some hypothesis splits on whose value the root's rows already force (a guard `?f = 0` where
|
|
555
|
+
// `?f = 1` holds, `k < len` where it is given). Split at the top, its forced side refutes at once; split late, under
|
|
556
|
+
// every other case, it is split again in each.
|
|
557
|
+
// GDP_UNITS=0 leaves them in the goal's order (for comparing).
|
|
558
|
+
const unitFirst = globalThis.process?.env?.GDP_UNITS !== '0';
|
|
559
|
+
let unitsNow = true;
|
|
560
|
+
const unitSeen = new WeakMap();
|
|
561
|
+
function unitsOf(goal) {
|
|
562
|
+
let out = unitSeen.get(goal);
|
|
563
|
+
if (out)
|
|
564
|
+
return out;
|
|
565
|
+
// Forced by the root's rows without a solver: a row the atom is (or is the negation of), or the bounds the rows give
|
|
566
|
+
// single variables (x <= c, x >= c) deciding it whichever way its variables go.
|
|
567
|
+
const root = new Map(), rows = leafConstraints(goal, root), seen = new Set(), given = new Set();
|
|
568
|
+
const lo = new Map(), hi = new Map();
|
|
569
|
+
for (const r of rows) {
|
|
570
|
+
given.add(canon(r.lin));
|
|
571
|
+
const vs = Object.keys(r.lin).filter(key => key !== '1');
|
|
572
|
+
if (vs.length !== 1)
|
|
573
|
+
continue;
|
|
574
|
+
const x = vs[0], c = r.lin[x], k = r.lin['1'] ?? 0, bound = -k / c;
|
|
575
|
+
if (c > 0)
|
|
576
|
+
hi.set(x, Math.min(hi.get(x) ?? Infinity, Math.floor(bound)));
|
|
577
|
+
else
|
|
578
|
+
lo.set(x, Math.max(lo.get(x) ?? -Infinity, Math.ceil(bound)));
|
|
579
|
+
}
|
|
580
|
+
const range = (lin) => Object.entries(lin).reduce(([a, b], [x, c]) => x === '1' ? [a + c, b + c]
|
|
581
|
+
: c > 0 ? [a + c * (lo.get(x) ?? -Infinity), b + c * (hi.get(x) ?? Infinity)] : [a + c * (hi.get(x) ?? Infinity), b + c * (lo.get(x) ?? -Infinity)], [0, 0]);
|
|
582
|
+
out = [];
|
|
583
|
+
for (const h of goal.hyps.slice(0, -1)) {
|
|
584
|
+
const atoms = [];
|
|
585
|
+
openH(h, root, atoms);
|
|
586
|
+
for (const lin of atoms) {
|
|
587
|
+
const id = canon(lin);
|
|
588
|
+
if (seen.has(id) || seen.has(canonFlip(lin)) || !Object.keys(lin).some(key => key !== '1'))
|
|
589
|
+
continue;
|
|
590
|
+
seen.add(id);
|
|
591
|
+
const [a, b] = range(lin);
|
|
592
|
+
if (given.has(id) || given.has(canon(atomOf(lin, false).lin)) || b <= 0 || a >= 1)
|
|
593
|
+
out.push(lin);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
unitSeen.set(goal, out);
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
459
599
|
// Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
|
|
460
600
|
const openSeen = new WeakMap();
|
|
461
601
|
// What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
|
|
@@ -826,6 +966,134 @@ function instancesFor(m, laws, build, opts = {}) {
|
|
|
826
966
|
}
|
|
827
967
|
return inst;
|
|
828
968
|
}
|
|
969
|
+
// Instances worth their rows: a quantified flag or a law about every key, at a key where the goals read a map the way
|
|
970
|
+
// its condition reads one at its own key (M[k] read as M[t], M[k + b] as M[t + b]). An instance anywhere else gives each
|
|
971
|
+
// leaf rows about cells nothing else mentions, and a split for each (a flag over grant numbers at a path's positions,
|
|
972
|
+
// say); a tuple, where each of its keys is so read. Found as a fixpoint from the goals with none of these instances, as
|
|
973
|
+
// an instance's rows read cells that make others worth it; a name whose condition reads no map at its keys, and the
|
|
974
|
+
// focus keys, are kept as they are.
|
|
975
|
+
// Untrusted: fewer instances still make a checked proof, and where they don't prove a goal the full set is tried.
|
|
976
|
+
const triggers = (body, key) => {
|
|
977
|
+
const out = [], mentions = (e) => JSON.stringify(e).includes(`"name":${JSON.stringify(key)}`);
|
|
978
|
+
const inE = (e) => { if (e.op === 'get') {
|
|
979
|
+
if (mentions(e.key))
|
|
980
|
+
out.push({ map: e.map, at: e.key });
|
|
981
|
+
inE(e.key);
|
|
982
|
+
}
|
|
983
|
+
else if (e.op === 'if')
|
|
984
|
+
(inC(e.c), inE(e.then), inE(e.else));
|
|
985
|
+
else if (e.op === 'mul')
|
|
986
|
+
inE(e.a);
|
|
987
|
+
else if ('a' in e)
|
|
988
|
+
(inE(e.a), inE(e.b)); };
|
|
989
|
+
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)));
|
|
990
|
+
inC(body);
|
|
991
|
+
return out;
|
|
992
|
+
};
|
|
993
|
+
const substKey = (e, key, by) => JSON.parse(JSON.stringify(e).replaceAll(`{"op":"var","name":${JSON.stringify(key)}}`, JSON.stringify(by)));
|
|
994
|
+
// GDP_TRIGGER=0 keeps every instance (for comparing).
|
|
995
|
+
const triggerOn = globalThis.process?.env?.GDP_TRIGGER !== '0';
|
|
996
|
+
function triggered(m, laws, inst, build, focus = []) {
|
|
997
|
+
if (!triggerOn)
|
|
998
|
+
return inst;
|
|
999
|
+
// Per name: its bound keys, and for each the triggers that read it alone (a key with none takes any value).
|
|
1000
|
+
const bodies = new Map();
|
|
1001
|
+
for (const [n, ks] of Object.entries(inst)) {
|
|
1002
|
+
if (!ks.length)
|
|
1003
|
+
continue;
|
|
1004
|
+
const q = n.startsWith('?') ? m.step.flags?.[n.slice(1)] : undefined, l = q ? undefined : laws.find(x => x.name === n);
|
|
1005
|
+
const body = q?.op === 'all' ? q.c : l?.forall ? l.holds : undefined, keys = q?.op === 'all' ? boundOf(q) : l?.forall ? [l.forall, ...(l.keys ?? [])] : [];
|
|
1006
|
+
if (!body)
|
|
1007
|
+
continue;
|
|
1008
|
+
const per = keys.map(key => triggers(body, key).filter(t => keys.every(o => o === key || !JSON.stringify(t.at).includes(`"name":${JSON.stringify(o)}`))));
|
|
1009
|
+
if (per.some(t => t.length))
|
|
1010
|
+
bodies.set(n, { keys, per });
|
|
1011
|
+
}
|
|
1012
|
+
if (!bodies.size)
|
|
1013
|
+
return inst;
|
|
1014
|
+
const tuplesOf = (n, ks) => { const a = bodies.get(n).keys.length; return Array.from({ length: Math.floor(ks.length / a) }, (_, t) => ks.slice(t * a, (t + 1) * a)); };
|
|
1015
|
+
const kept = (tuple) => tuple.some(e => e.op === 'var' && focus.includes(e.name));
|
|
1016
|
+
const out = Object.fromEntries(Object.entries(inst).map(([n, ks]) => [n, bodies.has(n) ? tuplesOf(n, ks).filter(kept).flat() : ks]));
|
|
1017
|
+
for (let round = 0; round < 8; round++) {
|
|
1018
|
+
const reads = new Set();
|
|
1019
|
+
const inE = (e) => { if (e.op === 'get') {
|
|
1020
|
+
reads.add(`${e.map}|${JSON.stringify(e.key)}`);
|
|
1021
|
+
inE(e.key);
|
|
1022
|
+
}
|
|
1023
|
+
else if (e.op === 'if')
|
|
1024
|
+
(inC(e.c), inE(e.then), inE(e.else));
|
|
1025
|
+
else if (e.op === 'mul')
|
|
1026
|
+
inE(e.a);
|
|
1027
|
+
else if ('a' in e)
|
|
1028
|
+
(inE(e.a), inE(e.b)); };
|
|
1029
|
+
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)));
|
|
1030
|
+
for (const g of build(out))
|
|
1031
|
+
g.hyps.forEach(inC);
|
|
1032
|
+
let grew = false;
|
|
1033
|
+
for (const [n, { keys, per }] of bodies) {
|
|
1034
|
+
const have = new Set(tuplesOf(n, out[n]).map(t => JSON.stringify(t)));
|
|
1035
|
+
for (const tuple of tuplesOf(n, inst[n])) {
|
|
1036
|
+
const id = JSON.stringify(tuple);
|
|
1037
|
+
if (have.has(id) || !keys.every((key, i) => !per[i].length || per[i].some(({ map, at }) => reads.has(`${map}|${JSON.stringify(substKey(at, key, tuple[i]))}`))))
|
|
1038
|
+
continue;
|
|
1039
|
+
out[n].push(...tuple);
|
|
1040
|
+
have.add(id);
|
|
1041
|
+
grew = true;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (!grew)
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
return out;
|
|
1048
|
+
}
|
|
1049
|
+
// For a certificate others check again (the Lean export sets `minimal.on`): the instances of quantified flags a proof
|
|
1050
|
+
// found that its leaves refute with, the proof grown again with those alone. The checked goal is then built from far
|
|
1051
|
+
// fewer hypotheses (a law about pairs of items with 900 instance hypotheses and 30 leaves using a few of them, say).
|
|
1052
|
+
export const minimal = { on: false };
|
|
1053
|
+
// The hypotheses a tree's leaves read: each leaf's input rows, and the rows its products multiply.
|
|
1054
|
+
function usedHyps(goal, tree, path = new Map(), out = new Set()) {
|
|
1055
|
+
if ('leaf' in tree) {
|
|
1056
|
+
const rows = leafConstraints(goal, path), n = rows.length, sq = tree.squares?.length ?? 0;
|
|
1057
|
+
const mark = (i) => {
|
|
1058
|
+
if (i < n) {
|
|
1059
|
+
const s = rows[i].src;
|
|
1060
|
+
if (s && 'fact' in s)
|
|
1061
|
+
out.add(s.fact);
|
|
1062
|
+
}
|
|
1063
|
+
else if (i >= n + sq) {
|
|
1064
|
+
const pq = tree.products?.[i - n - sq];
|
|
1065
|
+
if (pq) {
|
|
1066
|
+
mark(pq[0]);
|
|
1067
|
+
mark(pq[1]);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
for (const st of tree.leaf)
|
|
1072
|
+
if (st.kind === 'input')
|
|
1073
|
+
mark(st.index);
|
|
1074
|
+
return out;
|
|
1075
|
+
}
|
|
1076
|
+
const id = canon(tree.split);
|
|
1077
|
+
usedHyps(goal, tree.yes, new Map(path).set(id, { lin: tree.split, holds: true }), out);
|
|
1078
|
+
return usedHyps(goal, tree.no, new Map(path).set(id, { lin: tree.split, holds: false }), out);
|
|
1079
|
+
}
|
|
1080
|
+
// The tuples of each quantified flag whose hypotheses `used` names: a flag's instance hypotheses follow its ?f <= 1 in
|
|
1081
|
+
// the goal, in the instances' order (kernel.ts's closing).
|
|
1082
|
+
function usedInstances(m, goal, inst, used, focus) {
|
|
1083
|
+
const out = { ...inst };
|
|
1084
|
+
for (const [f, q] of Object.entries(m.step.flags ?? {})) {
|
|
1085
|
+
if (q.op !== 'all' || !inst[`?${f}`]?.length)
|
|
1086
|
+
continue;
|
|
1087
|
+
const at = goal.hyps.findIndex(h => h.op === '<=' && h.a.op === 'var' && h.a.name === `?${f}` && h.b.op === 'const' && h.b.value === 1);
|
|
1088
|
+
if (at < 0)
|
|
1089
|
+
continue;
|
|
1090
|
+
const a = boundOf(q).length, ks = inst[`?${f}`];
|
|
1091
|
+
out[`?${f}`] = Array.from({ length: Math.floor(ks.length / a) }, (_, t) => ks.slice(t * a, (t + 1) * a))
|
|
1092
|
+
.filter((tuple, t) => used.has(at + 1 + t) || tuple.some(e => e.op === 'var' && focus.includes(e.name))).flat();
|
|
1093
|
+
}
|
|
1094
|
+
return out;
|
|
1095
|
+
}
|
|
1096
|
+
const sameInstances = (a, b) => Object.keys(b).every(n => (a[n]?.length ?? 0) === b[n].length);
|
|
829
1097
|
// Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
|
|
830
1098
|
const avoiding = (inst, names, laws, m) => {
|
|
831
1099
|
const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
|
|
@@ -920,6 +1188,9 @@ export function proveLaw(m, laws, i) {
|
|
|
920
1188
|
seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
|
|
921
1189
|
focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
|
|
922
1190
|
});
|
|
1191
|
+
const few = triggered(m, laws, inst, i2 => obligations(m, laws, { [law.name]: i2 }, i), law.forall ? [`${law.forall}#`] : []);
|
|
1192
|
+
if (!sameInstances(few, inst) && obligations(m, laws, { [law.name]: few }, i).every(goal => 'tree' in grow(goal)))
|
|
1193
|
+
return { proven: true };
|
|
923
1194
|
for (const goal of obligations(m, laws, { [law.name]: inst }, i)) {
|
|
924
1195
|
const out = grow(goal);
|
|
925
1196
|
if (!('tree' in out))
|
|
@@ -936,6 +1207,64 @@ function proveLawsAsGiven(m, laws) {
|
|
|
936
1207
|
focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
|
|
937
1208
|
});
|
|
938
1209
|
});
|
|
1210
|
+
const t0 = performance.now(), out = proveThinned(m, laws, { ...instances }), usualMs = performance.now() - t0;
|
|
1211
|
+
if (cyclicOn()) {
|
|
1212
|
+
if (cyclicMachine(m, laws))
|
|
1213
|
+
crossCyclic('machine', m.name, out.proven && out.checked, () => { const { facts, specs } = machineNodes(m, laws, instances); return proveCyclic(facts, specs); }, usualMs);
|
|
1214
|
+
else
|
|
1215
|
+
console.error(`cyclic: ${JSON.stringify({ kind: 'machine', name: m.name, usual: out.proven && out.checked, uncovered: true })}`);
|
|
1216
|
+
}
|
|
1217
|
+
return out;
|
|
1218
|
+
}
|
|
1219
|
+
// Each law with its instances thinned (`triggered`) where that proves it, else with all of them.
|
|
1220
|
+
function proveThinned(m, laws, instances) {
|
|
1221
|
+
const trees = {}, fulls = { ...instances };
|
|
1222
|
+
let nodes = 0, leaves = 0;
|
|
1223
|
+
for (const [i, law] of laws.entries()) {
|
|
1224
|
+
const full = instances[law.name], few = triggered(m, laws, full, inst => obligations(m, laws, { ...instances, [law.name]: inst }, i), law.forall ? [`${law.forall}#`] : []);
|
|
1225
|
+
for (const inst of sameInstances(few, full) ? [full] : [few, full]) {
|
|
1226
|
+
instances[law.name] = inst;
|
|
1227
|
+
const done = {};
|
|
1228
|
+
let failed, n = 0, lv = 0;
|
|
1229
|
+
for (const goal of obligations(m, laws, instances, i)) {
|
|
1230
|
+
const out = grow(goal);
|
|
1231
|
+
if (!('tree' in out)) {
|
|
1232
|
+
failed = { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) };
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
done[goal.name] = out.tree;
|
|
1236
|
+
n += out.nodes;
|
|
1237
|
+
lv += countLeaves(out.tree);
|
|
1238
|
+
}
|
|
1239
|
+
if (failed && inst !== full)
|
|
1240
|
+
continue;
|
|
1241
|
+
if (failed)
|
|
1242
|
+
return { proven: false, nodes, leaves, failed, checked: false };
|
|
1243
|
+
Object.assign(trees, done);
|
|
1244
|
+
nodes += n;
|
|
1245
|
+
leaves += lv;
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// The goals of no one law (a history's own), with the instances chosen.
|
|
1250
|
+
for (const goal of obligations(m, laws, instances)) {
|
|
1251
|
+
if (trees[goal.name])
|
|
1252
|
+
continue;
|
|
1253
|
+
const out = grow(goal);
|
|
1254
|
+
// Where thinned instances leave such a goal unproven, every goal again with the full ones.
|
|
1255
|
+
if (!('tree' in out) && laws.some(l => instances[l.name] !== fulls[l.name]))
|
|
1256
|
+
return proveWith(m, laws, fulls);
|
|
1257
|
+
if (!('tree' in out))
|
|
1258
|
+
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
1259
|
+
trees[goal.name] = out.tree;
|
|
1260
|
+
nodes += out.nodes;
|
|
1261
|
+
leaves += countLeaves(out.tree);
|
|
1262
|
+
}
|
|
1263
|
+
const proof = { instances, trees };
|
|
1264
|
+
return { proven: true, nodes, leaves, checked: checkLaws(m, laws, proof), proof };
|
|
1265
|
+
}
|
|
1266
|
+
// Every goal of the laws with these instances.
|
|
1267
|
+
function proveWith(m, laws, instances) {
|
|
939
1268
|
const trees = {};
|
|
940
1269
|
let nodes = 0, leaves = 0;
|
|
941
1270
|
for (const goal of obligations(m, laws, instances)) {
|
|
@@ -986,7 +1315,12 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
|
|
|
986
1315
|
let nodes = base.nodes, leaves = base.leaves;
|
|
987
1316
|
for (const sl of stepLaws)
|
|
988
1317
|
for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
|
|
989
|
-
|
|
1318
|
+
const full = perStep[sl.name] ?? instances, few = triggered(plain, laws, full, inst => stepLawObligations(m, laws, inst, sl), [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])));
|
|
1319
|
+
let out = sameInstances(few, full) ? grow(goal) : grow(stepLawObligations(m, laws, few, sl).find(g => g.name === goal.name) ?? goal);
|
|
1320
|
+
if ('tree' in out && !sameInstances(few, full))
|
|
1321
|
+
perStep[sl.name] = few;
|
|
1322
|
+
else if (!sameInstances(few, full))
|
|
1323
|
+
out = grow(goal);
|
|
990
1324
|
// A k-step law that reads a condition kept across its steps: each flag at its own witnesses, at every step.
|
|
991
1325
|
if (!('tree' in out))
|
|
992
1326
|
for (const kept of keptAcross(plain, laws, perStep[sl.name] ?? instances, own(sl), sl.steps ?? 1)) {
|
|
@@ -999,6 +1333,18 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
|
|
|
999
1333
|
}
|
|
1000
1334
|
if (!('tree' in out))
|
|
1001
1335
|
return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
|
|
1336
|
+
if (minimal.on) {
|
|
1337
|
+
const had = perStep[sl.name] ?? instances, built = stepLawObligations(m, laws, had, sl).find(g => g.name === goal.name);
|
|
1338
|
+
if (built) {
|
|
1339
|
+
const focus = [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])), thin = usedInstances(plain, built, had, usedHyps(built, out.tree), focus);
|
|
1340
|
+
const g2 = sameInstances(thin, had) ? undefined : stepLawObligations(m, laws, thin, sl).find(g => g.name === goal.name);
|
|
1341
|
+
const again = g2 && grow(g2);
|
|
1342
|
+
if (again && 'tree' in again) {
|
|
1343
|
+
out = again;
|
|
1344
|
+
perStep[sl.name] = thin;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1002
1348
|
trees[goal.name] = out.tree;
|
|
1003
1349
|
nodes += out.nodes;
|
|
1004
1350
|
leaves += countLeaves(out.tree);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { C, E, Law, Machine, Recurrence } from '../core/machine.js';
|
|
2
2
|
import { type Lin, type Step } from '../core/solver.js';
|
|
3
3
|
import { type Fold, type Goal, type Instances, type Liveness, type Loop, type Path, type StepLaw, type Tree } from '../kernel/kernel.js';
|
|
4
|
+
import { type Cyclic } from '../kernel/cyclic.js';
|
|
4
5
|
import { leanLibrary } from './library.js';
|
|
5
6
|
export { leanLibrary };
|
|
6
7
|
export type Exported = {
|
|
@@ -143,6 +144,14 @@ export type LeanItem = {
|
|
|
143
144
|
} | {
|
|
144
145
|
id: string;
|
|
145
146
|
goal: Goal;
|
|
147
|
+
} | {
|
|
148
|
+
id: string;
|
|
149
|
+
cyclic: Cyclic;
|
|
150
|
+
tampered?: {
|
|
151
|
+
what: string;
|
|
152
|
+
proof: Cyclic;
|
|
153
|
+
}[];
|
|
154
|
+
after?: (ix: (name: string) => number, mx: (map: string) => number) => string;
|
|
146
155
|
};
|
|
147
156
|
export declare function toLean(items: LeanItem[], opts?: {
|
|
148
157
|
title?: string;
|
|
@@ -151,3 +160,7 @@ export declare function toLean(items: LeanItem[], opts?: {
|
|
|
151
160
|
export declare function leanProject(file: string, opts?: {
|
|
152
161
|
name?: string;
|
|
153
162
|
}): Record<string, string>;
|
|
163
|
+
export declare function cyclicToLean(id: string, p: Cyclic, tampered?: {
|
|
164
|
+
what: string;
|
|
165
|
+
proof: Cyclic;
|
|
166
|
+
}[], after?: (ix: (name: string) => number, mx: (map: string) => number) => string): string;
|
package/dist/lean-kernel/emit.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { atMost, n, not, pointWrite } from "../core/machine.js";
|
|
2
2
|
import { add, k, scale, sub } from "../core/solver.js";
|
|
3
3
|
import { boundOf, canon, factorsOf, isCell, isMonomial, columnTerm, stateName, termColumn, historyMachine, foldObligations, leafConstraints, loopLiveness, loopMachine, linear, livenessHelpful, livenessParts, livenessRanks, loopObligations, trueC, obligations, progressObligations, stepLawObligations, substC, witnessesOf, withProducts } from "../kernel/kernel.js";
|
|
4
|
-
import { grow, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
|
|
4
|
+
import { grow, minimal, proveBuilt, proveLaws, proveStepLaws } from "../kernel/prove.js";
|
|
5
5
|
import { refinement, refinementSides } from "../core/compose.js";
|
|
6
6
|
import { leanSurface, leanTerms } from "./lean-terms.js";
|
|
7
|
+
import { descentGoal, mainGoal, premiseGoal } from "../kernel/cyclic.js";
|
|
7
8
|
import { leanLibrary } from "./library.js";
|
|
8
9
|
export { leanLibrary };
|
|
9
10
|
// --- to Lean ------------------------------------------------------------------------------------------------------------
|
|
@@ -1055,11 +1056,15 @@ export function toLean(items, opts = {}) {
|
|
|
1055
1056
|
const outer = checkBy;
|
|
1056
1057
|
const env = globalThis.process?.env;
|
|
1057
1058
|
checkBy = (opts.native ?? env?.GDP_LEAN_NATIVE === '1') ? 'native_decide' : 'decide +kernel';
|
|
1059
|
+
// The proofs found with only the instances their leaves use: the same laws, from fewer hypotheses for Lean to build.
|
|
1060
|
+
const wasMinimal = minimal.on;
|
|
1061
|
+
minimal.on = true;
|
|
1058
1062
|
try {
|
|
1059
1063
|
return toLeanWith(items, opts);
|
|
1060
1064
|
}
|
|
1061
1065
|
finally {
|
|
1062
1066
|
checkBy = outer;
|
|
1067
|
+
minimal.on = wasMinimal;
|
|
1063
1068
|
}
|
|
1064
1069
|
}
|
|
1065
1070
|
function toLeanWith(items, opts) {
|
|
@@ -1119,6 +1124,10 @@ function toLeanWith(items, opts) {
|
|
|
1119
1124
|
parts.push(`-- ${item.id}\n${loopToLean(id, item.loop, item.laws ?? [], item.rank)}`);
|
|
1120
1125
|
theorems.push(`${id}_halts`);
|
|
1121
1126
|
}
|
|
1127
|
+
else if ('cyclic' in item) {
|
|
1128
|
+
parts.push(`-- ${item.id}\n${cyclicToLean(id, item.cyclic, item.tampered ?? [], item.after)}`);
|
|
1129
|
+
theorems.push(`${id}_sound`, ...(item.after ? [...item.after(() => 0, () => 0).matchAll(/^theorem (\S+)/gm)].map(x2 => x2[1]) : []));
|
|
1130
|
+
}
|
|
1122
1131
|
else if ('fold' in item) {
|
|
1123
1132
|
parts.push(`-- ${item.id}\n${foldToLean(id, item.fold, item.laws)}`);
|
|
1124
1133
|
theorems.push(`${id}_laws`);
|
|
@@ -1132,7 +1141,7 @@ function toLeanWith(items, opts) {
|
|
|
1132
1141
|
}
|
|
1133
1142
|
// The file the items need: Live.lean for a liveness or a loop, Sets.lean (which imports Steps.lean) for any machine.
|
|
1134
1143
|
return `import History
|
|
1135
|
-
import ${items.some(item => 'refinement' in item) ? 'Refine' : items.some(item => 'machine' in item && item.ghosts?.length) ? 'Ghost' : items.some(item => ('machine' in item && item.liveness?.length) || 'loop' in item) ? 'Live' : items.some(item => 'machine' in item) ? 'Sets' : 'Steps'}
|
|
1144
|
+
${items.some(item => 'cyclic' in item) ? 'import Cyclic\n' : ''}import ${items.some(item => 'refinement' in item) ? 'Refine' : items.some(item => 'machine' in item && item.ghosts?.length) ? 'Ghost' : items.some(item => ('machine' in item && item.liveness?.length) || 'loop' in item) ? 'Live' : items.some(item => 'machine' in item) ? 'Sets' : 'Steps'}
|
|
1136
1145
|
open Kern Cert
|
|
1137
1146
|
set_option maxRecDepth 100000
|
|
1138
1147
|
set_option maxHeartbeats 0
|
|
@@ -1164,3 +1173,42 @@ lean_lib ${name} where
|
|
|
1164
1173
|
`,
|
|
1165
1174
|
};
|
|
1166
1175
|
}
|
|
1176
|
+
// A cyclic proof through Kern.cyclic_sound: the facts and the nodes, each goal's table and tree (Lean builds the goals
|
|
1177
|
+
// from the nodes), one theorem for every node; each tampered proof's check computed to false. `after` writes Lean
|
|
1178
|
+
// after it (a corollary in plain terms), given the variables' and maps' numbers.
|
|
1179
|
+
export function cyclicToLean(id, p, tampered = [], after) {
|
|
1180
|
+
const all = [p, ...tampered.map(t => t.proof)];
|
|
1181
|
+
const names = (t) => [...JSON.stringify(t).matchAll(/"op":"var","name":"([^"]+)"/g)].map(m => m[1]);
|
|
1182
|
+
const vars = [...new Set(all.flatMap(q => [...q.facts.flatMap(f => [...f.keys, ...names(f.holds)]), ...q.nodes.flatMap(n => [...names([n.hyps, n.concl, n.rank, n.insts]), ...n.uses.flatMap(u => [...Object.keys(u.subst), ...names([u.subst, u.guard])])])]))];
|
|
1183
|
+
const maps = [...new Set([...JSON.stringify(all).matchAll(/"op":"get","map":"([^"]+)"/g)].map(m => m[1]))];
|
|
1184
|
+
const { e, c } = leanTerms(vars, maps, id);
|
|
1185
|
+
const ix = (name) => vars.indexOf(name);
|
|
1186
|
+
const facts = list(p.facts.map(f => `⟨${list(f.keys.map(key => String(ix(key))))}, ${c(f.holds)}⟩`));
|
|
1187
|
+
// A tampered proof keeps the proof's tables and trees (made for the proof's own goals) under its changed nodes.
|
|
1188
|
+
const enc = (g, tree) => { const x = encode({ name: `${id}: ${g.name}`, goal: g, tree }, false, true, vars, maps); return `(${x.tab}, ${x.tree})`; };
|
|
1189
|
+
const nodeLits = (q) => q.nodes.map((n, i) => {
|
|
1190
|
+
const o = p.nodes[i];
|
|
1191
|
+
const uses = n.uses.map((u, j) => {
|
|
1192
|
+
const ou = o.uses[j], om = p.nodes[ou.node];
|
|
1193
|
+
return `⟨${u.node}, ${list(Object.entries(u.subst).map(([name, t]) => `(${ix(name)}, ${e(t)})`))}, ${c(u.guard)},\n ${list(om.hyps.map((h, k2) => enc(premiseGoal(p.facts, o, ou, h, k2), ou.premises[k2])))},\n ${enc(descentGoal(p.facts, o, ou, om), ou.descent)}⟩`;
|
|
1194
|
+
});
|
|
1195
|
+
return { hyps: list(n.hyps.map(c)), concl: c(n.concl), rest: `${list(n.rank.map(e))}, ${list(n.insts.map(t => `(${t.fact}, ${list(t.at.map(e))})`))},\n ${list(uses)},\n ${enc(mainGoal(p.facts, p.nodes, o), o.main)}` };
|
|
1196
|
+
});
|
|
1197
|
+
const nodes = (q) => list(nodeLits(q).map(x => `⟨${x.hyps}, ${x.concl}, ${x.rest}⟩`));
|
|
1198
|
+
const mx = (map) => maps.indexOf(map);
|
|
1199
|
+
return `-- ${p.nodes.map(n => n.name).join('; ')} (variables ${vars.map((name, j) => `${j} ${name}`).join(', ')}; maps ${maps.map((name, j) => `${j} ${name}`).join(', ')})
|
|
1200
|
+
def ${id}_facts : List Fact := ${facts}
|
|
1201
|
+
${nodeLits(p).map((x, i) => `def ${id}_hyps${i} : List C := ${x.hyps}
|
|
1202
|
+
def ${id}_concl${i} : C := ${x.concl}
|
|
1203
|
+
def ${id}_node${i} : CNode := ⟨${id}_hyps${i}, ${id}_concl${i}, ${x.rest}⟩
|
|
1204
|
+
theorem ${id}_node${i}_hyps : ${id}_node${i}.hyps = ${id}_hyps${i} := rfl
|
|
1205
|
+
theorem ${id}_node${i}_concl : ${id}_node${i}.concl = ${id}_concl${i} := rfl`).join('\n')}
|
|
1206
|
+
def ${id}_nodes : List CNode := ${list(p.nodes.map((_, i) => `${id}_node${i}`))}
|
|
1207
|
+
${p.nodes.map((_, i) => `theorem ${id}_node${i}_mem : ${id}_node${i} ∈ ${id}_nodes := by simp only [${id}_nodes, List.mem_cons, true_or, or_true]`).join('\n')}
|
|
1208
|
+
theorem ${id}_check : checkCyclic ${id}_facts ${id}_nodes = true := by ${checkBy}
|
|
1209
|
+
theorem ${id}_sound : ∀ (maps : Nat → Nat → Nat) env, FactsHold ${id}_facts env maps → ∀ n ∈ ${id}_nodes, (∀ c ∈ n.hyps, evalC env maps c = true) → evalC env maps n.concl = true :=
|
|
1210
|
+
cyclic_sound ${id}_facts ${id}_nodes ${id}_check
|
|
1211
|
+
${tampered.map((t, j) => `-- TAMPERED: ${t.what}. Lean computes the check to false.
|
|
1212
|
+
def ${id}_tampered${j} : List CNode := ${nodes(t.proof)}
|
|
1213
|
+
example : checkCyclic ${id}_facts ${id}_tampered${j} = false := by ${checkBy}`).join('\n')}${after ? `\n${after(ix, mx)}` : ''}`;
|
|
1214
|
+
}
|