gdp-wired 0.16.0 → 0.18.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,7 @@
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 { checkRefutation } from "../core/check.js";
3
+ import { crossCyclic, cyclicMachine, cyclicOn, machineNodes, proveCyclic } from "../core/cyclic.js";
4
+ 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
5
  // The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
4
6
  function openE(e, path, out) {
5
7
  switch (e.op) {
@@ -76,7 +78,10 @@ function openH(c, path, out, positive = true) {
76
78
  if (out.length === before && !positive && c.op === '=' && truth(c, path) === undefined)
77
79
  openC(c, path, out);
78
80
  }
79
- // Pairs of reads of one map whose keys the path doesn't yet compare.
81
+ // Pairs of reads of one map whose keys the path doesn't yet compare. With `pairedKeys.skip` (core/data.ts), reads at
82
+ // keys a pairing builds ((a + b)² + b, keys whose difference has monomials) are left uncompared: that encoding states
83
+ // their congruence as facts, and comparing their keys only brings products into leaves its linear rows decide.
84
+ export const pairedKeys = { skip: false };
80
85
  function openCells(rows, path) {
81
86
  const names = [...new Set(rows.flatMap(r => Object.keys(r.lin)))].filter(isCell);
82
87
  const out = [];
@@ -86,7 +91,27 @@ function openCells(rows, path) {
86
91
  if (p.slice(0, p.indexOf('[')) !== q.slice(0, q.indexOf('[')))
87
92
  continue;
88
93
  const d = sub(parse(keyText(p)), parse(keyText(q)));
94
+ if (pairedKeys.skip && Object.keys(d).some(isMonomial))
95
+ continue;
89
96
  const dec = (lin) => path.get(canon(lin))?.holds ?? (path.has(canonFlip(lin)) ? !path.get(canonFlip(lin)).holds : undefined);
97
+ // Two cells of a run's map (core/cyclic.ts: time u and key k at u + k·Ω) at keys k·Ω and k'·Ω: k = k' first, a
98
+ // linear atom; the keys' own difference only where k = k' (its other side then refuted by (k − k')·Ω = 0, a product).
99
+ const q0 = cellProducts.on ? quotient(d) : undefined;
100
+ if (q0) {
101
+ const qa = dec(q0), qb = dec(scale(q0, -1));
102
+ if (qa === undefined) {
103
+ out.push(q0);
104
+ continue;
105
+ }
106
+ if (qa === true && qb === undefined) {
107
+ out.push(scale(q0, -1));
108
+ continue;
109
+ }
110
+ if (!(qa && qb))
111
+ continue;
112
+ cellDiffs.set(canon(d), q0);
113
+ cellDiffs.set(canon(scale(d, -1)), scale(q0, -1));
114
+ }
90
115
  const a = dec(d), b = dec(scale(d, -1));
91
116
  if (a === undefined)
92
117
  out.push(d);
@@ -95,6 +120,24 @@ function openCells(rows, path) {
95
120
  }
96
121
  return out;
97
122
  }
123
+ // A key difference q·F over monomials that share one factor F (Ω), with no other part: q, else undefined.
124
+ const quotient = (d) => {
125
+ const keys = Object.keys(d);
126
+ if (!keys.length || keys.some(key => key === '1' || !isMonomial(key)))
127
+ return undefined;
128
+ const shared = factorsOf(keys[0]).find(f => keys.every(key => factorsOf(key).includes(f)));
129
+ if (!shared)
130
+ return undefined;
131
+ const out = {};
132
+ for (const key of keys) {
133
+ const fs = [...factorsOf(key)];
134
+ fs.splice(fs.indexOf(shared), 1);
135
+ out[fs.join('*')] = (out[fs.join('*')] ?? 0) + d[key];
136
+ }
137
+ return out;
138
+ };
139
+ // The cells' key differences split at their quotient's equality, by canonical text: their quotient.
140
+ const cellDiffs = new Map();
98
141
  function parse(text) {
99
142
  const out = {};
100
143
  let depth = 0, start = 0;
@@ -138,10 +181,45 @@ const solveP = (rows, maxRows) => {
138
181
  return out;
139
182
  };
140
183
  // PROVE_TRACE=1 logs each goal grown from its root (hypotheses, nodes, time, outcome) to stderr.
184
+ // PROVE_DUMP: a hook research/perf/prof sets to read each root goal.
185
+ export let proveDump;
186
+ export const setProveDump = (f) => { proveDump = f; };
141
187
  const proveTrace = !!globalThis.process?.env?.PROVE_TRACE;
188
+ // `why`, when the budget ran out: which limit, at what size, and what usually helps.
189
+ // Set while core/cyclic.ts grows its goals.
190
+ export const cellProducts = { on: false };
142
191
  // The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
143
192
  // is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
193
+ // A deadline every goal grown inside `withDeadline` keeps, beside its own budget: a search that proves many things (core/
194
+ // infer.ts, core/discover.ts, the search proveRule falls back on) bounds them all; a goal past it is left `unknown`.
195
+ let deadline = Infinity;
196
+ export function withDeadline(until, f) {
197
+ const outer = deadline;
198
+ deadline = Math.min(outer, until);
199
+ try {
200
+ return f();
201
+ }
202
+ finally {
203
+ deadline = outer;
204
+ }
205
+ }
206
+ // `units: false`: the goal's own order, no units first (a caller after a model, not a proof: a run guided by the
207
+ // prover's witnesses, say, whose models units would steer).
144
208
  export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
209
+ if (deadline < (budget.until ?? Infinity))
210
+ budget.until = deadline;
211
+ if (budget.units === false && unitsNow) {
212
+ unitsNow = false;
213
+ try {
214
+ return grow(goal, path, budget);
215
+ }
216
+ finally {
217
+ unitsNow = true;
218
+ }
219
+ }
220
+ if (proveDump && !path.size && !budget.nodes) {
221
+ proveDump(goal);
222
+ }
145
223
  if (proveTrace && !path.size && !budget.nodes) {
146
224
  const t = performance.now(), out = growRoot(goal, path, budget);
147
225
  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 +235,18 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
157
235
  function growRoot(goal, path, budget) {
158
236
  // At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
159
237
  // two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
238
+ if (path.size === 0 && budget.nodes === 0 && unitFirst && unitsNow) {
239
+ // A goal that a few cases settle needs no units (finding them costs a Farkas check per atom); the rest get them.
240
+ unitsNow = false;
241
+ try {
242
+ const quick = growPlain(goal, path, { nodes: 0, until: budget.until, cap: 64 });
243
+ if ('tree' in quick || quick.stuck === 'sat' || quick.why === undefined)
244
+ return quick;
245
+ }
246
+ finally {
247
+ unitsNow = true;
248
+ }
249
+ }
160
250
  if (path.size === 0 && budget.nodes === 0) {
161
251
  const open = new Set(leafConstraints(goal, path).flatMap(r => Object.keys(r.lin)).filter(n => n.startsWith('⟦') && columnTerm(n)?.op === 'if')).size;
162
252
  const plain = growPlain(goal, path, { nodes: 0, until: budget.until, cap: Math.max(32, Math.round(4_000 / 2 ** (open / 2))) });
@@ -177,7 +267,7 @@ function growPlain(goal, path, budget) {
177
267
  return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
178
268
  // Rows that read a term column only once a cut has put one on the path; before, the rows a choice-free search uses
179
269
  // (what it saw before term columns), renumbered to the whole leaf's when a leaf is found.
180
- const full = timed(() => leafConstraintsReusing(goal, path), ms => { profile.rowsMs += ms; });
270
+ const full = timed(() => rowsAt(goal, path), ms => { profile.rowsMs += ms; });
181
271
  const columns = [...path.values()].some(a => Object.keys(a.lin).some(key => key.startsWith('⟦')));
182
272
  const idx = full.flatMap((r, i) => (columns || !Object.keys(r.lin).some(key => key.startsWith('⟦')) ? [i] : []));
183
273
  const rows = idx.map(i => full[i]), at = (i) => (i < rows.length ? idx[i] : full.length + i - rows.length);
@@ -196,6 +286,8 @@ function growPlain(goal, path, budget) {
196
286
  byHyp.set(h, (seen = {})); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
197
287
  // Then the hypotheses about only the names the conclusion reads (an assumption about another key, a witness, can
198
288
  // wait: a refutation seldom needs it), then the rest, each group in the goal's order.
289
+ if (unitFirst && unitsNow)
290
+ open.push(...unitsOf(goal));
199
291
  if (last >= 0)
200
292
  open.push(...openOf(hyps[last]));
201
293
  const near = nearOf(goal);
@@ -228,6 +320,21 @@ function growPlain(goal, path, budget) {
228
320
  return { tree: { leaf: fast.steps }, nodes: budget.nodes };
229
321
  }
230
322
  let split = next ?? (verdict.kind === 'unknown' ? bisect(rows, path) ?? fractional(verdict.point, path) : undefined);
323
+ // A cell difference's failing side (k = k' on the path, the keys apart): (k − k')·Ω = 0 refutes it, a product of each
324
+ // of the path's two atoms k − k' <= 0 and k' − k <= 0 with Ω >= 0.
325
+ if (cellProducts.on && path.size) {
326
+ const [id, last] = [...path].at(-1), q0 = !last.holds ? cellDiffs.get(id) : undefined;
327
+ if (q0) {
328
+ const at = (lin) => rows.findIndex(r => canon(r.lin) === canon(lin));
329
+ const d = last.lin, f = factorsOf(Object.keys(d)[0]).find(x => !Object.keys(q0).includes(x) && Object.keys(d).every(key => factorsOf(key).includes(x)));
330
+ const e1 = at(q0), e2 = at(scale(q0, -1)), sign = f === undefined ? -1 : at({ [f]: -1 });
331
+ if (e1 >= 0 && e2 >= 0 && sign >= 0) {
332
+ const pairs = [[e1, sign], [e2, sign]], out = solve(withProducts(rows, pairs));
333
+ if (out.kind === 'refuted')
334
+ return { tree: leafOf(out.steps, pairs), nodes: budget.nodes };
335
+ }
336
+ }
337
+ }
231
338
  // Products: with monomials about, try every pair of rows multiplied; failing that, split a factor at zero.
232
339
  const monomials = rows.some(r => Object.keys(r.lin).some(isMonomial));
233
340
  // A witness that multiplies out (each monomial's value is its factors' product) is a real model: nothing to refute.
@@ -237,6 +344,30 @@ function growPlain(goal, path, budget) {
237
344
  if (real)
238
345
  return { stuck: 'sat', witness: w, path };
239
346
  }
347
+ // 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
348
+ // 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.
349
+ // Only for cyclic proofs (core/cyclic.ts, whose runs keep each next state as a cell): the usual rules substitute it.
350
+ if (!split && monomials && cellProducts.on) {
351
+ const factorSet = new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)));
352
+ const byCanon = new Map(rows.map((r, i) => [canon(r.lin), i]));
353
+ 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] : []));
354
+ 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] : []; });
355
+ const pairs = eqs.flatMap(i => signs.map(j => [i, j])).slice(0, 200);
356
+ if (pairs.length) {
357
+ const sub = solve(withProducts(rows, pairs));
358
+ if (sub.kind === 'refuted')
359
+ return { tree: leafOf(sub.steps, pairs), nodes: budget.nodes };
360
+ // Then every linear row about a factor (n >= x: (n − x)·d >= 0) times each sign, past the forty rows the pairs
361
+ // below are kept to.
362
+ 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] : []));
363
+ const more = [...pairs, ...about.flatMap(i => signs.map(j => [i, j]))].slice(0, 400);
364
+ if (more.length > pairs.length) {
365
+ const ineq = solve(withProducts(rows, more));
366
+ if (ineq.kind === 'refuted')
367
+ return { tree: leafOf(ineq.steps, more), nodes: budget.nodes };
368
+ }
369
+ }
370
+ }
240
371
  if (!split && monomials && rows.length <= 40) {
241
372
  // Only rows about the factors (no other map cells), each pair once: elimination over hundreds of products blows up.
242
373
  const factorSet = new Set(rows.flatMap(r => Object.keys(r.lin).filter(isMonomial).flatMap(factorsOf)));
@@ -426,6 +557,24 @@ function squareForms(rows) {
426
557
  // the decision's own row, and any row only it makes, are not there), every split kept. The derivations are the same
427
558
  // arithmetic over the same rows, and the kernel checks the whole tree in the end. Undefined at the first leaf that
428
559
  // needs the decision.
560
+ // A leaf's rows, by its path's decisions in order (what they are a function of): a lift reads every leaf of a subtree
561
+ // at its own path, found when it was grown, and at the path without the split, which the next lift up reads again.
562
+ const rowsSeen = new WeakMap();
563
+ function rowsAt(goal, path) {
564
+ let byPath = rowsSeen.get(goal);
565
+ if (!byPath)
566
+ rowsSeen.set(goal, (byPath = new Map()));
567
+ let id = '';
568
+ for (const [key, a] of path)
569
+ id += `${a.holds ? '+' : '-'}${key}|`;
570
+ let rows = byPath.get(id);
571
+ if (!rows) {
572
+ if (byPath.size >= 4096)
573
+ byPath.clear();
574
+ byPath.set(id, (rows = leafConstraintsReusing(goal, path)));
575
+ }
576
+ return rows;
577
+ }
429
578
  function lift(goal, tree, from, to) {
430
579
  const own = to.size; // the decision's row: the last of `from`'s path rows, below every later split's
431
580
  const reads = (t) => ('leaf' in t ? t.leaf.some(st => st.kind === 'input' && st.index === own) || (t.products ?? []).some(([i, j]) => i === own || j === own) : reads(t.yes) || reads(t.no));
@@ -440,7 +589,7 @@ function lift(goal, tree, from, to) {
440
589
  const no = yes && go(t.no, new Map(a).set(sid, { lin: t.split, holds: false }), new Map(b).set(sid, { lin: t.split, holds: false }));
441
590
  return yes && no ? { split: t.split, yes, no } : undefined;
442
591
  }
443
- const ra = leafConstraintsReusing(goal, a), rb = leafConstraintsReusing(goal, b), at = new Map();
592
+ const ra = rowsAt(goal, a), rb = rowsAt(goal, b), at = new Map();
444
593
  rb.forEach((r, i) => { const key = canon(r.lin); if (!at.has(key))
445
594
  at.set(key, i); });
446
595
  const map = (i) => (i < ra.length ? at.get(canon(ra[i].lin)) : undefined);
@@ -466,6 +615,86 @@ function lift(goal, tree, from, to) {
466
615
  };
467
616
  return go(tree, from, to);
468
617
  }
618
+ // A tree for a weaker goal (some hypotheses left out, some conjuncts of one left out) made a tree for the whole: at any
619
+ // path a leaf's rows are among the whole goal's (each hypothesis' facts are a hypothesis' own, the cells, terms and
620
+ // naturals they read a subset), so each input is renumbered to the same row there, a square or product past the rows
621
+ // moved by the difference. Where a dropped conjunct makes the whole hypothesis false under the path, its row 1 <= 0
622
+ // refutes the leaf alone; a leaf neither maps is grown again on the whole goal.
623
+ // `opaque`: variables the weaker goal has for terms of the whole (a step's value, read the same in two hypotheses), each
624
+ // read as the term's linear form at the whole goal's path; a split the whole's path already decides is followed, not
625
+ // made again.
626
+ export function widen(part, whole, tree, opaque = {}, pp = new Map(), wp = pp) {
627
+ const names = Object.keys(opaque);
628
+ const rename = (lin) => {
629
+ if (!names.length)
630
+ return lin;
631
+ let out = {};
632
+ for (const [key, c] of Object.entries(lin)) {
633
+ const e = opaque[key];
634
+ if (e) {
635
+ const l = linear(e, wp);
636
+ if (!l)
637
+ return undefined;
638
+ out = add(out, scale(l, c));
639
+ }
640
+ else if (names.some(n => key.includes(n)))
641
+ return undefined;
642
+ else
643
+ out = add(out, { [key]: c });
644
+ }
645
+ return out;
646
+ };
647
+ if (!('leaf' in tree)) {
648
+ const id = canon(tree.split), wl = rename(tree.split);
649
+ if (!wl) {
650
+ const o = grow(whole, wp);
651
+ return 'tree' in o ? o.tree : undefined;
652
+ }
653
+ const wid = canon(wl), known = Object.keys(wl).every(key => key === '1') ? (wl['1'] ?? 0) <= 0 : wp.has(wid) ? wp.get(wid).holds : wp.has(canonFlip(wl)) ? !wp.get(canonFlip(wl)).holds : undefined;
654
+ if (known !== undefined)
655
+ return widen(part, whole, known ? tree.yes : tree.no, opaque, new Map(pp).set(id, { lin: tree.split, holds: known }), wp);
656
+ const yes = widen(part, whole, tree.yes, opaque, new Map(pp).set(id, { lin: tree.split, holds: true }), new Map(wp).set(wid, { lin: wl, holds: true }));
657
+ const no = yes && widen(part, whole, tree.no, opaque, new Map(pp).set(id, { lin: tree.split, holds: false }), new Map(wp).set(wid, { lin: wl, holds: false }));
658
+ return yes && no ? { split: wl, yes, no } : undefined;
659
+ }
660
+ const rp = rowsAt(part, pp), rw = rowsAt(whole, wp), at = new Map();
661
+ rw.forEach((r, i) => { const key = canon(r.lin); if (!at.has(key))
662
+ at.set(key, i); });
663
+ const map = (i) => { if (i >= rp.length)
664
+ return i - rp.length + rw.length; const l = rename(rp[i].lin); return l && at.get(canon(l)); };
665
+ const leaf = [], products = [], squares = (tree.squares ?? []).map(rename);
666
+ let ok = squares.every(Boolean);
667
+ for (const st of ok ? tree.leaf : []) {
668
+ if (st.kind !== 'input') {
669
+ leaf.push(st);
670
+ continue;
671
+ }
672
+ const j = map(st.index);
673
+ if (j === undefined) {
674
+ ok = false;
675
+ break;
676
+ }
677
+ leaf.push({ ...st, index: j });
678
+ }
679
+ for (const [i, j] of ok ? tree.products ?? [] : []) {
680
+ const pi = map(i), pj = map(j);
681
+ if (pi === undefined || pj === undefined) {
682
+ ok = false;
683
+ break;
684
+ }
685
+ products.push([pi, pj]);
686
+ }
687
+ // (renamed, a derivation through products or cuts is replayed: a cut's divisor may not divide the terms' coefficients)
688
+ if (ok && names.length && !checkRefutation(withProducts(rw, products, squares), leaf))
689
+ ok = false;
690
+ if (ok)
691
+ return { leaf, ...(tree.products ? { products } : {}), ...(tree.squares ? { squares: squares } : {}) };
692
+ const absurd = rw.findIndex(r => Object.keys(r.lin).every(key => key === '1') && (r.lin['1'] ?? 0) > 0);
693
+ if (absurd >= 0)
694
+ return { leaf: [{ kind: 'input', index: absurd }] };
695
+ const out = grow(whole, wp);
696
+ return 'tree' in out ? out.tree : undefined;
697
+ }
469
698
  // Whether each hypothesis reads only names the goal's conclusion (its last hypothesis) reads.
470
699
  const nearSeen = new WeakMap();
471
700
  const namesOf = (t) => (t === undefined ? [] : [...JSON.stringify(t).matchAll(/"(?:name|map)":"([^"]+)"/g)].map(m => m[1]));
@@ -477,6 +706,51 @@ function nearOf(goal) {
477
706
  }
478
707
  return out;
479
708
  }
709
+ // Units first: an atom some hypothesis splits on whose value the root's rows already force (a guard `?f = 0` where
710
+ // `?f = 1` holds, `k < len` where it is given). Split at the top, its forced side refutes at once; split late, under
711
+ // every other case, it is split again in each.
712
+ // GDP_UNITS=0 leaves them in the goal's order (for comparing).
713
+ const unitFirst = globalThis.process?.env?.GDP_UNITS !== '0';
714
+ let unitsNow = true;
715
+ const unitSeen = new WeakMap();
716
+ function unitsOf(goal) {
717
+ let out = unitSeen.get(goal);
718
+ if (out)
719
+ return out;
720
+ // 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
721
+ // single variables (x <= c, x >= c) deciding it whichever way its variables go.
722
+ const root = new Map(), rows = leafConstraints(goal, root), seen = new Set(), given = new Set();
723
+ const lo = new Map(), hi = new Map();
724
+ for (const r of rows) {
725
+ given.add(canon(r.lin));
726
+ const vs = Object.keys(r.lin).filter(key => key !== '1');
727
+ if (vs.length !== 1)
728
+ continue;
729
+ const x = vs[0], c = r.lin[x], k = r.lin['1'] ?? 0, bound = -k / c;
730
+ if (c > 0)
731
+ hi.set(x, Math.min(hi.get(x) ?? Infinity, Math.floor(bound)));
732
+ else
733
+ lo.set(x, Math.max(lo.get(x) ?? -Infinity, Math.ceil(bound)));
734
+ }
735
+ const range = (lin) => Object.entries(lin).reduce(([a, b], [x, c]) => x === '1' ? [a + c, b + c]
736
+ : 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]);
737
+ out = [];
738
+ for (const h of goal.hyps.slice(0, -1)) {
739
+ const atoms = [];
740
+ openH(h, root, atoms);
741
+ for (const lin of atoms) {
742
+ const id = canon(lin);
743
+ if (seen.has(id) || seen.has(canonFlip(lin)) || !Object.keys(lin).some(key => key !== '1'))
744
+ continue;
745
+ seen.add(id);
746
+ const [a, b] = range(lin);
747
+ if (given.has(id) || given.has(canon(atomOf(lin, false).lin)) || b <= 0 || a >= 1)
748
+ out.push(lin);
749
+ }
750
+ }
751
+ unitSeen.set(goal, out);
752
+ return out;
753
+ }
480
754
  // Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
481
755
  const openSeen = new WeakMap();
482
756
  // 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 +1121,134 @@ function instancesFor(m, laws, build, opts = {}) {
847
1121
  }
848
1122
  return inst;
849
1123
  }
1124
+ // Instances worth their rows: a quantified flag or a law about every key, at a key where the goals read a map the way
1125
+ // 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
1126
+ // leaf rows about cells nothing else mentions, and a split for each (a flag over grant numbers at a path's positions,
1127
+ // say); a tuple, where each of its keys is so read. Found as a fixpoint from the goals with none of these instances, as
1128
+ // an instance's rows read cells that make others worth it; a name whose condition reads no map at its keys, and the
1129
+ // focus keys, are kept as they are.
1130
+ // Untrusted: fewer instances still make a checked proof, and where they don't prove a goal the full set is tried.
1131
+ const triggers = (body, key) => {
1132
+ const out = [], mentions = (e) => JSON.stringify(e).includes(`"name":${JSON.stringify(key)}`);
1133
+ const inE = (e) => { if (e.op === 'get') {
1134
+ if (mentions(e.key))
1135
+ out.push({ map: e.map, at: e.key });
1136
+ inE(e.key);
1137
+ }
1138
+ else if (e.op === 'if')
1139
+ (inC(e.c), inE(e.then), inE(e.else));
1140
+ else if (e.op === 'mul')
1141
+ inE(e.a);
1142
+ else if ('a' in e)
1143
+ (inE(e.a), inE(e.b)); };
1144
+ 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)));
1145
+ inC(body);
1146
+ return out;
1147
+ };
1148
+ const substKey = (e, key, by) => JSON.parse(JSON.stringify(e).replaceAll(`{"op":"var","name":${JSON.stringify(key)}}`, JSON.stringify(by)));
1149
+ // GDP_TRIGGER=0 keeps every instance (for comparing).
1150
+ const triggerOn = globalThis.process?.env?.GDP_TRIGGER !== '0';
1151
+ function triggered(m, laws, inst, build, focus = []) {
1152
+ if (!triggerOn)
1153
+ return inst;
1154
+ // Per name: its bound keys, and for each the triggers that read it alone (a key with none takes any value).
1155
+ const bodies = new Map();
1156
+ for (const [n, ks] of Object.entries(inst)) {
1157
+ if (!ks.length)
1158
+ continue;
1159
+ const q = n.startsWith('?') ? m.step.flags?.[n.slice(1)] : undefined, l = q ? undefined : laws.find(x => x.name === n);
1160
+ const body = q?.op === 'all' ? q.c : l?.forall ? l.holds : undefined, keys = q?.op === 'all' ? boundOf(q) : l?.forall ? [l.forall, ...(l.keys ?? [])] : [];
1161
+ if (!body)
1162
+ continue;
1163
+ const per = keys.map(key => triggers(body, key).filter(t => keys.every(o => o === key || !JSON.stringify(t.at).includes(`"name":${JSON.stringify(o)}`))));
1164
+ if (per.some(t => t.length))
1165
+ bodies.set(n, { keys, per });
1166
+ }
1167
+ if (!bodies.size)
1168
+ return inst;
1169
+ 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)); };
1170
+ const kept = (tuple) => tuple.some(e => e.op === 'var' && focus.includes(e.name));
1171
+ const out = Object.fromEntries(Object.entries(inst).map(([n, ks]) => [n, bodies.has(n) ? tuplesOf(n, ks).filter(kept).flat() : ks]));
1172
+ for (let round = 0; round < 8; round++) {
1173
+ const reads = new Set();
1174
+ const inE = (e) => { if (e.op === 'get') {
1175
+ reads.add(`${e.map}|${JSON.stringify(e.key)}`);
1176
+ inE(e.key);
1177
+ }
1178
+ else if (e.op === 'if')
1179
+ (inC(e.c), inE(e.then), inE(e.else));
1180
+ else if (e.op === 'mul')
1181
+ inE(e.a);
1182
+ else if ('a' in e)
1183
+ (inE(e.a), inE(e.b)); };
1184
+ 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)));
1185
+ for (const g of build(out))
1186
+ g.hyps.forEach(inC);
1187
+ let grew = false;
1188
+ for (const [n, { keys, per }] of bodies) {
1189
+ const have = new Set(tuplesOf(n, out[n]).map(t => JSON.stringify(t)));
1190
+ for (const tuple of tuplesOf(n, inst[n])) {
1191
+ const id = JSON.stringify(tuple);
1192
+ 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]))}`))))
1193
+ continue;
1194
+ out[n].push(...tuple);
1195
+ have.add(id);
1196
+ grew = true;
1197
+ }
1198
+ }
1199
+ if (!grew)
1200
+ break;
1201
+ }
1202
+ return out;
1203
+ }
1204
+ // For a certificate others check again (the Lean export sets `minimal.on`): the instances of quantified flags a proof
1205
+ // found that its leaves refute with, the proof grown again with those alone. The checked goal is then built from far
1206
+ // fewer hypotheses (a law about pairs of items with 900 instance hypotheses and 30 leaves using a few of them, say).
1207
+ export const minimal = { on: false };
1208
+ // The hypotheses a tree's leaves read: each leaf's input rows, and the rows its products multiply.
1209
+ export function usedHyps(goal, tree, path = new Map(), out = new Set()) {
1210
+ if ('leaf' in tree) {
1211
+ const rows = rowsAt(goal, path), n = rows.length, sq = tree.squares?.length ?? 0;
1212
+ const mark = (i) => {
1213
+ if (i < n) {
1214
+ const s = rows[i].src;
1215
+ if (s && 'fact' in s)
1216
+ out.add(s.fact);
1217
+ }
1218
+ else if (i >= n + sq) {
1219
+ const pq = tree.products?.[i - n - sq];
1220
+ if (pq) {
1221
+ mark(pq[0]);
1222
+ mark(pq[1]);
1223
+ }
1224
+ }
1225
+ };
1226
+ for (const st of tree.leaf)
1227
+ if (st.kind === 'input')
1228
+ mark(st.index);
1229
+ return out;
1230
+ }
1231
+ const id = canon(tree.split);
1232
+ usedHyps(goal, tree.yes, new Map(path).set(id, { lin: tree.split, holds: true }), out);
1233
+ return usedHyps(goal, tree.no, new Map(path).set(id, { lin: tree.split, holds: false }), out);
1234
+ }
1235
+ // The tuples of each quantified flag whose hypotheses `used` names: a flag's instance hypotheses follow its ?f <= 1 in
1236
+ // the goal, in the instances' order (kernel.ts's closing).
1237
+ function usedInstances(m, goal, inst, used, focus) {
1238
+ const out = { ...inst };
1239
+ for (const [f, q] of Object.entries(m.step.flags ?? {})) {
1240
+ if (q.op !== 'all' || !inst[`?${f}`]?.length)
1241
+ continue;
1242
+ const at = goal.hyps.findIndex(h => h.op === '<=' && h.a.op === 'var' && h.a.name === `?${f}` && h.b.op === 'const' && h.b.value === 1);
1243
+ if (at < 0)
1244
+ continue;
1245
+ const a = boundOf(q).length, ks = inst[`?${f}`];
1246
+ out[`?${f}`] = Array.from({ length: Math.floor(ks.length / a) }, (_, t) => ks.slice(t * a, (t + 1) * a))
1247
+ .filter((tuple, t) => used.has(at + 1 + t) || tuple.some(e => e.op === 'var' && focus.includes(e.name))).flat();
1248
+ }
1249
+ return out;
1250
+ }
1251
+ const sameInstances = (a, b) => Object.keys(b).every(n => (a[n]?.length ?? 0) === b[n].length);
850
1252
  // Instances without the keys that read any of these names (a law about several keys loses each tuple that does).
851
1253
  const avoiding = (inst, names, laws, m) => {
852
1254
  const bad = (e) => names.some(n => JSON.stringify(e).includes(`"name":${JSON.stringify(n)}`));
@@ -941,6 +1343,9 @@ export function proveLaw(m, laws, i) {
941
1343
  seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
942
1344
  focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
943
1345
  });
1346
+ const few = triggered(m, laws, inst, i2 => obligations(m, laws, { [law.name]: i2 }, i), law.forall ? [`${law.forall}#`] : []);
1347
+ if (!sameInstances(few, inst) && obligations(m, laws, { [law.name]: few }, i).every(goal => 'tree' in grow(goal)))
1348
+ return { proven: true };
944
1349
  for (const goal of obligations(m, laws, { [law.name]: inst }, i)) {
945
1350
  const out = grow(goal);
946
1351
  if (!('tree' in out))
@@ -957,6 +1362,64 @@ function proveLawsAsGiven(m, laws) {
957
1362
  focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
958
1363
  });
959
1364
  });
1365
+ const t0 = performance.now(), out = proveThinned(m, laws, { ...instances }), usualMs = performance.now() - t0;
1366
+ if (cyclicOn()) {
1367
+ if (cyclicMachine(m, laws))
1368
+ crossCyclic('machine', m.name, out.proven && out.checked, () => { const { facts, specs } = machineNodes(m, laws, instances); return proveCyclic(facts, specs); }, usualMs);
1369
+ else
1370
+ console.error(`cyclic: ${JSON.stringify({ kind: 'machine', name: m.name, usual: out.proven && out.checked, uncovered: true })}`);
1371
+ }
1372
+ return out;
1373
+ }
1374
+ // Each law with its instances thinned (`triggered`) where that proves it, else with all of them.
1375
+ function proveThinned(m, laws, instances) {
1376
+ const trees = {}, fulls = { ...instances };
1377
+ let nodes = 0, leaves = 0;
1378
+ for (const [i, law] of laws.entries()) {
1379
+ const full = instances[law.name], few = triggered(m, laws, full, inst => obligations(m, laws, { ...instances, [law.name]: inst }, i), law.forall ? [`${law.forall}#`] : []);
1380
+ for (const inst of sameInstances(few, full) ? [full] : [few, full]) {
1381
+ instances[law.name] = inst;
1382
+ const done = {};
1383
+ let failed, n = 0, lv = 0;
1384
+ for (const goal of obligations(m, laws, instances, i)) {
1385
+ const out = grow(goal);
1386
+ if (!('tree' in out)) {
1387
+ failed = { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) };
1388
+ break;
1389
+ }
1390
+ done[goal.name] = out.tree;
1391
+ n += out.nodes;
1392
+ lv += countLeaves(out.tree);
1393
+ }
1394
+ if (failed && inst !== full)
1395
+ continue;
1396
+ if (failed)
1397
+ return { proven: false, nodes, leaves, failed, checked: false };
1398
+ Object.assign(trees, done);
1399
+ nodes += n;
1400
+ leaves += lv;
1401
+ break;
1402
+ }
1403
+ }
1404
+ // The goals of no one law (a history's own), with the instances chosen.
1405
+ for (const goal of obligations(m, laws, instances)) {
1406
+ if (trees[goal.name])
1407
+ continue;
1408
+ const out = grow(goal);
1409
+ // Where thinned instances leave such a goal unproven, every goal again with the full ones.
1410
+ if (!('tree' in out) && laws.some(l => instances[l.name] !== fulls[l.name]))
1411
+ return proveWith(m, laws, fulls);
1412
+ if (!('tree' in out))
1413
+ return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
1414
+ trees[goal.name] = out.tree;
1415
+ nodes += out.nodes;
1416
+ leaves += countLeaves(out.tree);
1417
+ }
1418
+ const proof = { instances, trees };
1419
+ return { proven: true, nodes, leaves, checked: checkLaws(m, laws, proof), proof };
1420
+ }
1421
+ // Every goal of the laws with these instances.
1422
+ function proveWith(m, laws, instances) {
960
1423
  const trees = {};
961
1424
  let nodes = 0, leaves = 0;
962
1425
  for (const goal of obligations(m, laws, instances)) {
@@ -1007,7 +1470,12 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
1007
1470
  let nodes = base.nodes, leaves = base.leaves;
1008
1471
  for (const sl of stepLaws)
1009
1472
  for (const goal of stepLawObligations(m, laws, perStep[sl.name] ?? instances, sl)) {
1010
- let out = grow(goal);
1473
+ 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] : [])));
1474
+ let out = sameInstances(few, full) ? grow(goal) : grow(stepLawObligations(m, laws, few, sl).find(g => g.name === goal.name) ?? goal);
1475
+ if ('tree' in out && !sameInstances(few, full))
1476
+ perStep[sl.name] = few;
1477
+ else if (!sameInstances(few, full))
1478
+ out = grow(goal);
1011
1479
  // A k-step law that reads a condition kept across its steps: each flag at its own witnesses, at every step.
1012
1480
  if (!('tree' in out))
1013
1481
  for (const kept of keptAcross(plain, laws, perStep[sl.name] ?? instances, own(sl), sl.steps ?? 1)) {
@@ -1020,6 +1488,18 @@ function proveStepLawsAsGiven(m, laws, stepLaws, base) {
1020
1488
  }
1021
1489
  if (!('tree' in out))
1022
1490
  return { proven: false, nodes, leaves, failed: { goal: goal.name, stuck: out.stuck, ...(out.witness ? { witness: out.witness } : {}), ...(out.why ? { why: out.why } : {}) }, checked: false };
1491
+ if (minimal.on) {
1492
+ const had = perStep[sl.name] ?? instances, built = stepLawObligations(m, laws, had, sl).find(g => g.name === goal.name);
1493
+ if (built) {
1494
+ const focus = [sl.forall, ...(sl.keys ?? [])].flatMap(k => (k ? [k] : [])), thin = usedInstances(plain, built, had, usedHyps(built, out.tree), focus);
1495
+ const g2 = sameInstances(thin, had) ? undefined : stepLawObligations(m, laws, thin, sl).find(g => g.name === goal.name);
1496
+ const again = g2 && grow(g2);
1497
+ if (again && 'tree' in again) {
1498
+ out = again;
1499
+ perStep[sl.name] = thin;
1500
+ }
1501
+ }
1502
+ }
1023
1503
  trees[goal.name] = out.tree;
1024
1504
  nodes += out.nodes;
1025
1505
  leaves += countLeaves(out.tree);