gdp-wired 0.16.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.
@@ -1,5 +1,6 @@
1
1
  import { add, k, refuteFast, scale, solve, sub, v } from "../core/solver.js";
2
- import { assumed, 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";
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)));
@@ -477,6 +551,51 @@ function nearOf(goal) {
477
551
  }
478
552
  return out;
479
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
+ }
480
599
  // Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
481
600
  const openSeen = new WeakMap();
482
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.
@@ -847,6 +966,134 @@ function instancesFor(m, laws, build, opts = {}) {
847
966
  }
848
967
  return inst;
849
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);
850
1097
  // Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
851
1098
  const avoiding = (inst, names, laws, m) => {
852
1099
  const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
@@ -941,6 +1188,9 @@ export function proveLaw(m, laws, i) {
941
1188
  seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
942
1189
  focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
943
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 };
944
1194
  for (const goal of obligations(m, laws, { [law.name]: inst }, i)) {
945
1195
  const out = grow(goal);
946
1196
  if (!('tree' in out))
@@ -957,6 +1207,64 @@ function proveLawsAsGiven(m, laws) {
957
1207
  focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
958
1208
  });
959
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) {
960
1268
  const trees = {};
961
1269
  let nodes = 0, leaves = 0;
962
1270
  for (const goal of obligations(m, laws, instances)) {
@@ -1007,7 +1315,12 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
1007
1315
  let nodes = base.nodes, leaves = base.leaves;
1008
1316
  for (const sl of stepLaws)
1009
1317
  for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
1010
- let out = grow(goal);
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);
1011
1324
  // A k-step law that reads a condition kept across its steps: each flag at its own witnesses, at every step.
1012
1325
  if (!('tree' in out))
1013
1326
  for (const kept of keptAcross(plain, laws, perStep[sl.name] ?? instances, own(sl), sl.steps ?? 1)) {
@@ -1020,6 +1333,18 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
1020
1333
  }
1021
1334
  if (!('tree' in out))
1022
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
+ }
1023
1348
  trees[goal.name] = out.tree;
1024
1349
  nodes += out.nodes;
1025
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;
@@ -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
+ }