gdp-wired 0.19.0 → 0.21.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,9 +1,9 @@
1
- import { fnValue, pairedArgs } from "./machine.js";
2
- import { check, livenessParts, loopLiveness, loopMachine, loopObligations, substC, substE } from "../kernel/kernel.js";
3
- import { factsAvoid, lexLt } from "../kernel/cyclic.js";
1
+ import { compileFunction, evalC as evalMC, evalE as evalME, fnValue, interpret, pairedArgs } from "./machine.js";
2
+ import { check, livenessHelpful, livenessParts, loopLiveness, loopMachine, loopObligations, substC, substE } from "../kernel/kernel.js";
3
+ import { checkCyclic, factsAvoid, lexLt } from "../kernel/cyclic.js";
4
4
  import { proveCyclic } from "./cyclic.js";
5
5
  import { solve } from "./solver.js";
6
- import { grow, proveLaws, proveStepLaws, withDeadline } from "../kernel/prove.js";
6
+ import { grow, proveLaws, proveLiveness, proveStepLaws, withDeadline } from "../kernel/prove.js";
7
7
  import { inferLaws } from "./infer.js";
8
8
  // --- terms ----------------------------------------------------------------------------------------------------------
9
9
  const v = (name) => ({ op: 'var', name }), k = (value) => ({ op: 'const', value });
@@ -36,7 +36,10 @@ export const showE = (e) => {
36
36
  case 'add': return `${showE(e.a)} + ${showE(e.b)}`;
37
37
  case 'sub': return `${showE(e.a)} − ${e.b.op === 'add' || e.b.op === 'sub' ? `(${showE(e.b)})` : showE(e.b)}`;
38
38
  case 'mul': return `${e.k}·${e.a.op === 'var' || e.a.op === 'const' ? showE(e.a) : `(${showE(e.a)})`}`;
39
- case 'prod': return `(${showE(e.a)})·(${showE(e.b)})`;
39
+ case 'prod': {
40
+ const f = (t) => (t.op === 'var' || t.op === 'const' || t.op === 'get' ? showE(t) : `(${showE(t)})`);
41
+ return `${f(e.a)}·${f(e.b)}`;
42
+ }
40
43
  case 'min':
41
44
  case 'max': return `${e.op}(${showE(e.a)}, ${showE(e.b)})`;
42
45
  case 'if': return `if ${showC(e.c)} then ${showE(e.then)} else ${showE(e.else)}`;
@@ -409,9 +412,10 @@ function lp(must, pool, until, scale = 1, signs = [true, false]) {
409
412
  }
410
413
  return undefined;
411
414
  };
412
- // The widest box first: where it has no solution, none of the smaller has.
415
+ // The widest box first: where it has no solution, none of the smaller has. Past it, a rank's rows (homogeneous in the
416
+ // unknowns, the drop's constant aside) are solved exactly over the rationals, the solution scaled to whole numbers.
413
417
  if (!stage(signs.at(-1), bounds.at(-1), true))
414
- return undefined;
418
+ return fit ? undefined : exactLp(must, pool, until);
415
419
  for (const natural of signs)
416
420
  for (const bound of bounds)
417
421
  for (const wide of [false, true]) {
@@ -421,14 +425,176 @@ function lp(must, pool, until, scale = 1, signs = [true, false]) {
421
425
  }
422
426
  return undefined;
423
427
  }
428
+ const bgcd = (a, b) => { if (a < 0n)
429
+ a = -a; if (b < 0n)
430
+ b = -b; while (b)
431
+ [a, b] = [b, a % b]; return a; };
432
+ const q = (n, d = 1n) => { if (d < 0n)
433
+ (n = -n, d = -d); const g = bgcd(n, d) || 1n; return g === 1n ? { n, d } : { n: n / g, d: d / g }; };
434
+ const Q0 = q(0n), Q1 = q(1n);
435
+ const qadd = (a, b) => (a.d === b.d ? q(a.n + b.n, a.d) : q(a.n * b.d + b.n * a.d, a.d * b.d));
436
+ const qsub = (a, b) => qadd(a, { n: -b.n, d: b.d });
437
+ const qmul = (a, b) => (!a.n || !b.n ? Q0 : q(a.n * b.n, a.d * b.d));
438
+ const qdiv = (a, b) => q(a.n * b.d, a.d * b.n);
439
+ const qlt = (a, b) => a.n * b.d < b.n * a.d;
440
+ // The primal simplex on a tableau whose basis is feasible: minimize cost·y over T·y = rhs (the last column), y >= 0,
441
+ // Bland's rule (the smallest column that improves, the smallest basis index among ties), only `open` columns entering.
442
+ function simplex(T, basis, cost, open, until) {
443
+ const cols = cost.length;
444
+ for (let it = 0; it < 5000; it++) {
445
+ if (performance.now() > until)
446
+ return false;
447
+ let enter = -1;
448
+ for (let j = 0; j < cols && enter < 0; j++) {
449
+ if (!open(j) || basis.includes(j))
450
+ continue;
451
+ let r = cost[j];
452
+ for (const [i, b] of basis.entries())
453
+ if (T[i][j].n && cost[b].n)
454
+ r = qsub(r, qmul(cost[b], T[i][j]));
455
+ if (r.n < 0n)
456
+ enter = j;
457
+ }
458
+ if (enter < 0)
459
+ return true;
460
+ let leave = -1, best;
461
+ for (const [i, row] of T.entries()) {
462
+ if (row[enter].n <= 0n)
463
+ continue;
464
+ const ratio = qdiv(row[cols], row[enter]);
465
+ if (!best || qlt(ratio, best) || (!qlt(best, ratio) && basis[i] < basis[leave]))
466
+ (best = ratio, leave = i);
467
+ }
468
+ if (leave < 0)
469
+ return false; // unbounded: not for an objective bounded below
470
+ const p = T[leave][enter];
471
+ T[leave] = T[leave].map(x => qdiv(x, p));
472
+ for (const [i, row] of T.entries()) {
473
+ if (i === leave || !row[enter].n)
474
+ continue;
475
+ const f = row[enter];
476
+ T[i] = row.map((x, j) => qsub(x, qmul(f, T[leave][j])));
477
+ }
478
+ basis[leave] = enter;
479
+ }
480
+ return false;
481
+ }
482
+ // The rows (lin <= 0) over free unknowns, solved exactly: Σ|u| least (each u as p − q, p, q >= 0), two phases. The
483
+ // rational solution scaled by its denominators' least common multiple and divided by its numerators' common divisor;
484
+ // kept only if every row holds of it exactly (a row with a positive constant, a drop of at least 1, holds of any
485
+ // multiple at least 1 of a solution; one with a negative constant may not).
486
+ function exactSolve(rows, until) {
487
+ const xs = [...new Set(rows.flatMap(l => Object.keys(l)).filter(x => x !== '1'))].sort(), n = xs.length, m = rows.length;
488
+ // Columns: p (n), q (n), a slack per row (m), an artificial per row (m); the right-hand side last.
489
+ const cols = 2 * n + 2 * m, T = [], basis = [];
490
+ for (const [i, l] of rows.entries()) {
491
+ let rhs = -(l['1'] ?? 0);
492
+ const sign = rhs < 0 ? -1 : 1;
493
+ rhs *= sign;
494
+ const row = Array.from({ length: cols + 1 }, () => Q0);
495
+ for (const [j, x] of xs.entries()) {
496
+ const c = sign * (l[x] ?? 0);
497
+ if (c)
498
+ (row[j] = q(BigInt(c)), row[n + j] = q(BigInt(-c)));
499
+ }
500
+ row[2 * n + i] = q(BigInt(sign));
501
+ if (sign < 0) {
502
+ row[2 * n + m + i] = Q1;
503
+ basis.push(2 * n + m + i);
504
+ }
505
+ else
506
+ basis.push(2 * n + i);
507
+ row[cols] = q(BigInt(rhs));
508
+ T.push(row);
509
+ }
510
+ const art = (j) => j >= 2 * n + m;
511
+ const phase1 = Array.from({ length: cols }, (_, j) => (art(j) ? Q1 : Q0));
512
+ if (!simplex(T, basis, phase1, () => true, until))
513
+ return undefined;
514
+ if (T.some((row, i) => art(basis[i]) && row[cols].n))
515
+ return undefined; // infeasible
516
+ // An artificial left in the basis at zero: pivoted out on any other column, or its row dropped (redundant).
517
+ for (let i = T.length - 1; i >= 0; i--) {
518
+ if (!art(basis[i]))
519
+ continue;
520
+ const j = T[i].findIndex((x, c) => c < 2 * n + m && x.n !== 0n);
521
+ if (j < 0) {
522
+ T.splice(i, 1);
523
+ basis.splice(i, 1);
524
+ continue;
525
+ }
526
+ const p = T[i][j];
527
+ T[i] = T[i].map(x => qdiv(x, p));
528
+ for (const [r, row] of T.entries())
529
+ if (r !== i && row[j].n) {
530
+ const f = row[j];
531
+ T[r] = row.map((x, c) => qsub(x, qmul(f, T[i][c])));
532
+ }
533
+ basis[i] = j;
534
+ }
535
+ const phase2 = Array.from({ length: cols }, (_, j) => (j < 2 * n ? Q1 : Q0));
536
+ if (!simplex(T, basis, phase2, j => !art(j), until))
537
+ return undefined;
538
+ const val = (j) => { const i = basis.indexOf(j); return i < 0 ? Q0 : T[i][cols]; };
539
+ const u = xs.map((_, j) => qsub(val(j), val(n + j)));
540
+ let lcm = 1n;
541
+ for (const r of u)
542
+ lcm = lcm / bgcd(lcm, r.d) * r.d;
543
+ const ints = u.map(r => r.n * (lcm / r.d)), g = ints.reduce((a, b) => bgcd(a, b), 0n) || 1n;
544
+ const w = {};
545
+ for (const [j, x] of xs.entries()) {
546
+ const c = Number(ints[j] / g);
547
+ if (!Number.isSafeInteger(c))
548
+ return undefined;
549
+ if (c)
550
+ w[x] = c;
551
+ }
552
+ return rows.every(l => valueAt(l, w) <= 0) ? w : undefined;
553
+ }
554
+ // exactSolve over `must` and the pool's rows, the pool's added as they are violated (the most violated first). The
555
+ // solution is found on the smaller half of the runs (each row's size its largest coefficient, a sample's value or a
556
+ // turn's change) and kept only where it holds of all of them: a coefficient the runs' size forces (32·x + y where x
557
+ // reaches 33, a reset y := x − 1 in the runs) grows with the runs, and is refused, where one a turn's change forces
558
+ // (11·x + 2·y, six tokens for a coin and a coin for five) is the same on both.
559
+ function exactLp(must, pool, until) {
560
+ const size = (l) => { let m = 0; for (const x in l)
561
+ if (x !== '1')
562
+ m = Math.max(m, Math.abs(l[x])); return m; };
563
+ const sizes = pool.map(size), half = sizes.reduce((a, b) => Math.max(a, b), 0) / 2, small = pool.filter((_, i) => sizes[i] <= half), rows = [...must];
564
+ for (let round = 0; round < 40 && performance.now() < until; round++) {
565
+ const w = exactSolve(rows, until);
566
+ if (!w)
567
+ return undefined;
568
+ // The twelve most violated, in one pass.
569
+ const bad = [];
570
+ for (const l of small) {
571
+ const x = valueAt(l, w);
572
+ if (x <= 0 || (bad.length === 12 && x <= bad[11][0]))
573
+ continue;
574
+ bad.push([x, l]);
575
+ bad.sort((p, q) => q[0] - p[0]);
576
+ if (bad.length > 12)
577
+ bad.pop();
578
+ }
579
+ if (!bad.length)
580
+ return pool.every(l => valueAt(l, w) <= 0) ? w : undefined;
581
+ rows.push(...bad.map(b => b[1]));
582
+ }
583
+ return undefined;
584
+ }
424
585
  // Lexicographic ranks over the edges (each from a node at a sample to a node at its arguments), every component >= 0
425
586
  // at every sample: Bradley–Manna–Sipma's successive elimination, each component the sum of one solution per edge it can
426
587
  // make drop. An edge no component makes drop is left (its use's guard keeps it where the rank drops, if anywhere).
427
- function lexRanks(idx, nodes, samples, edges, name, until) {
428
- const points = [...new Map(idx.flatMap(n => samples[n].map(s => { const l = rankRow(n, nodes, s.env, name, -1); return [id(l), l]; }))).values()];
588
+ // `onEdges`: each component a natural only where an edge compares it (at its ends), not at every sample: a rank the
589
+ // kernel reads truncated (n·n − i, 0 past the square, where no call is made). `weak`: edges the rank must not raise but
590
+ // need not lower (a liveness's steps other than its helpful requests'), each dropped once a component lowers it.
591
+ function lexRanks(idx, nodes, samples, edges, name, until, onEdges = false, weak = []) {
592
+ const ends = onEdges ? edges.flatMap(e => [[e.from, e.s], [e.to, e.t]]) : idx.flatMap(n => samples[n].map(s => [n, s.env]));
593
+ const points = [...new Map(ends.map(([n, env]) => { const l = rankRow(n, nodes, env, name, -1); return [id(l), l]; })).values()];
429
594
  const delta = (e) => plus(rankRow(e.to, nodes, e.t, name), rankRow(e.from, nodes, e.s, name, -1));
430
595
  const scale = 1 + Math.max(0, ...idx.flatMap(n => samples[n].flatMap(s => Object.values(s.env))));
431
596
  let left = [...new Map(edges.map(e => { const l = delta(e); return [id(l), l]; })).values()];
597
+ let keep = [...new Map(weak.map(e => { const l = delta(e); return [id(l), l]; })).values()].filter(l => !left.some(x => id(x) === id(l)));
432
598
  const comps = [];
433
599
  while (left.length && comps.length < 4 && performance.now() < until) {
434
600
  let sum = {};
@@ -439,7 +605,7 @@ function lexRanks(idx, nodes, samples, edges, name, until) {
439
605
  if (strict(l) || performance.now() > until || tried.has(sign(l)) || tried.size > 40)
440
606
  continue;
441
607
  tried.add(sign(l));
442
- const w = lp([plus(l, { 1: 1 })], [...left, ...points], until, scale);
608
+ const w = lp([plus(l, { 1: 1 })], [...left, ...keep, ...points], until, scale);
443
609
  if (w)
444
610
  sum = plus(sum, w);
445
611
  }
@@ -451,6 +617,7 @@ function lexRanks(idx, nodes, samples, edges, name, until) {
451
617
  sum = Object.fromEntries(Object.entries(sum).map(([x, c]) => [x, c / g]));
452
618
  comps.push(sum);
453
619
  const kept = left.filter(l => !strict(l));
620
+ keep = keep.filter(l => !strict(l));
454
621
  if (kept.length === left.length)
455
622
  break;
456
623
  left = kept;
@@ -458,7 +625,51 @@ function lexRanks(idx, nodes, samples, edges, name, until) {
458
625
  return { comps, left: left.length };
459
626
  }
460
627
  const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
461
- const compE = (c, n, node, name) => linE(Object.fromEntries(node.vars.map((x, j) => [x, c[name(n, j)] ?? 0])), c[name(n, 'b')] ?? 0);
628
+ // The same over named naturals and the edges of runs alone (core/data.ts: a recursive function's calls, each from its
629
+ // parameters' sizes to its arguments'): each component's coefficient per variable and its constant.
630
+ export function ranksOfRuns(vars, points, edges, ms = 1000) {
631
+ const node = { name: 'runs', hyps: [], concl: yes, vars, level: 0, given: true };
632
+ const { comps, left } = lexRanks([0], [node], [points.map(env => ({ env, calls: new Set() }))], edges.map(e => ({ from: 0, to: 0, s: e.s, t: e.t })), shared, performance.now() + ms);
633
+ return { comps: comps.map(c => ({ coef: Object.fromEntries(vars.map((x, j) => [x, c[shared(0, j)] ?? 0])), c: c[shared(0, 'b')] ?? 0 })), left };
634
+ }
635
+ const compE = (c, n, node, name) => unfeat(linE(Object.fromEntries(node.vars.map((x, j) => [x, c[name(n, j)] ?? 0])), c[name(n, 'b')] ?? 0));
636
+ // Ranks of degree 2: a product of two of a node's variables (a square too) a further variable of the LP, its value at
637
+ // each sample the product's, one product at a time (a sparse template: with every product at once, the runs fit a rank
638
+ // of many small parts that no turn past them keeps). The rank found is linear in it, the product read back as the term
639
+ // it is (x·y as prod(x, y)), which the kernel takes as a monomial, its goals the same linear goals over it (with its
640
+ // products of rows and squares, Positivstellensatz of degree 2, where they need more).
641
+ const featSep = '·';
642
+ const products = (names) => names.flatMap((a, i) => names.slice(i).map(b => [a, b]));
643
+ const featName = ([a, b]) => `${a}${featSep}${b}`;
644
+ const withFeat = (env, p) => (p && env[p[0]] !== undefined && env[p[1]] !== undefined ? { ...env, [featName(p)]: env[p[0]] * env[p[1]] } : env);
645
+ // (linE builds sums, differences and multiples of variables and constants only)
646
+ const unfeat = (e) => {
647
+ if (e.op === 'var') {
648
+ const [a, b] = e.name.split(featSep);
649
+ return b === undefined ? e : { op: 'prod', a: v(a), b: v(b) };
650
+ }
651
+ if (e.op === 'add' || e.op === 'sub')
652
+ return { ...e, a: unfeat(e.a), b: unfeat(e.b) };
653
+ return e.op === 'mul' ? { ...e, a: unfeat(e.a) } : e;
654
+ };
655
+ // The products of two variables a term reads, each as its sorted pair.
656
+ const productsIn = (t) => {
657
+ const out = new Map();
658
+ walk(t, o => { if (o.op === 'prod') {
659
+ const a = o.a, b = o.b;
660
+ if (a.op === 'var' && b.op === 'var') {
661
+ const p = [a.name, b.name].sort();
662
+ out.set(id(p), p);
663
+ }
664
+ } });
665
+ return [...out.values()];
666
+ };
667
+ // The nodes, samples and edges with one product of two variables per node a variable (none where `ps[i]` is undefined).
668
+ const withProducts = (nodes, samples, edges, ps) => ({
669
+ nodes: nodes.map((n, i) => (ps[i] ? { ...n, vars: [...n.vars, featName(ps[i])] } : n)),
670
+ samples: samples.map((ss, i) => ss.map(s => ({ ...s, env: withFeat(s.env, ps[i]) }))),
671
+ edges: edges.map(e => ({ ...e, s: withFeat(e.s, ps[e.from]), t: withFeat(e.t, ps[e.to]) })),
672
+ });
462
673
  // Strongly connected components (Tarjan), each node's component's index.
463
674
  function sccs(graph) {
464
675
  const index = [], low = [], on = [], stack = [], comp = [];
@@ -490,7 +701,8 @@ function sccs(graph) {
490
701
  visit(u);
491
702
  return comp;
492
703
  }
493
- function build(facts, cx, nodes, depth, big, until) {
704
+ // `quad`: ranks of degree 2, the product of those two variables a further one of each node that has both.
705
+ function build(facts, cx, nodes, depth, big, until, quad) {
494
706
  const inst = nodes.map(n => instancesFor(facts, cx, n, depth));
495
707
  const samples = [];
496
708
  for (const n of nodes) {
@@ -539,21 +751,23 @@ function build(facts, cx, nodes, depth, big, until) {
539
751
  parts.set(i, []);
540
752
  continue;
541
753
  }
542
- let best = { ...lexRanks(idx, nodes, samples, within, shared, until), name: shared };
754
+ const q = quad ? withProducts(nodes, samples, within, nodes.map(n => (n.vars.includes(quad[0]) && n.vars.includes(quad[1]) ? quad : undefined))) : { nodes, samples, edges: within };
755
+ let best = { ...lexRanks(idx, q.nodes, q.samples, q.edges, shared, until, !!quad), name: shared };
543
756
  if (best.left && idx.length > 1 && idx.length <= 4) {
544
- const o = lexRanks(idx, nodes, samples, within, own(nodes), until);
757
+ const o = lexRanks(idx, q.nodes, q.samples, q.edges, own(q.nodes), until, !!quad);
545
758
  if (o.left < best.left)
546
- best = { ...o, name: own(nodes) };
759
+ best = { ...o, name: own(q.nodes) };
547
760
  }
548
761
  for (const i of idx)
549
- parts.set(i, best.comps.map(cp => compE(cp, i, nodes[i], best.name)));
762
+ parts.set(i, best.comps.map(cp => compE(cp, i, q.nodes[i], best.name)));
550
763
  }
551
764
  const width = Math.max(0, ...[...parts.values()].map(p => p.length)), tiers = graph.some(g => g.length);
552
765
  const ranks = nodes.map((_, i) => [...(tiers ? [k(heightOf(comp[i]))] : []), ...parts.get(i), ...Array.from({ length: width - parts.get(i).length }, () => k(0))]);
553
766
  let uses = 0;
554
767
  const specs = nodes.map((n, i) => ({
555
768
  name: n.name, hyps: n.hyps, concl: n.concl, rank: ranks[i], insts: inst[i].insts,
556
- uses: cands[i].filter((_, u) => active[i][u]).map(c => {
769
+ // (a use at a rank that is this node's own, term for term, never applies: its guard is false, so it is left out)
770
+ uses: cands[i].filter((c, u) => active[i][u] && id(ranks[c.node].map(r => substE(r, vs(c.subst)))) !== id(ranks[i])).map(c => {
557
771
  uses++;
558
772
  const m = nodes[c.node], s = vs(c.subst);
559
773
  return { node: c.node, subst: c.subst, guard: and(...m.hyps.map(h => substC(h, s)), lexLt(ranks[c.node].map(r => substE(r, s)), ranks[i])) };
@@ -672,8 +886,8 @@ export function findCycles(facts, claims, opts = {}) {
672
886
  const big = Math.max(0, ...constsOf([facts.map(f => f.holds), claims]).filter(c => c <= 1000));
673
887
  const given = claims.map(c => nodeOf(c, 1, !opts.houdini));
674
888
  let last = { proven: false, checked: false, why: 'nothing tried' };
675
- const once = (nodes, depth) => {
676
- const b = withDeadline(until, () => build(facts, cx, nodes, depth, big, until));
889
+ const once = (nodes, depth, quad) => {
890
+ const b = withDeadline(until, () => build(facts, cx, nodes, depth, big, until, quad));
677
891
  if ('broken' in b) {
678
892
  const at = nodes.find(n => n.name === b.broken.node);
679
893
  return { failed: at, verdict: { proven: false, checked: false, goal: at.name, stuck: 'sat', witness: b.broken.env, why: 'the facts, run, break it' } };
@@ -695,10 +909,10 @@ export function findCycles(facts, claims, opts = {}) {
695
909
  };
696
910
  // Houdini over the nodes that may be dropped (each that fails, dropped, the rest proven again); then each companion
697
911
  // dropped in turn where the rest are proven without it (only what the claims need is kept).
698
- const attempt = (start, depth) => {
912
+ const attempt = (start, depth, quad) => {
699
913
  let nodes = start, done;
700
914
  for (let round = 0; round <= start.length && nodes.length && performance.now() < until && !done; round++) {
701
- const t = once(nodes, depth);
915
+ const t = once(nodes, depth, quad);
702
916
  if ('proven' in t) {
703
917
  done = t.proven;
704
918
  break;
@@ -714,14 +928,16 @@ export function findCycles(facts, claims, opts = {}) {
714
928
  for (const c of nodes.filter(n => !n.given)) {
715
929
  if (performance.now() >= until)
716
930
  break;
717
- const rest = nodes.filter(n => n !== c), t = once(rest, depth);
931
+ const rest = nodes.filter(n => n !== c), t = once(rest, depth, quad);
718
932
  if ('proven' in t && t.proven.checked)
719
933
  (nodes = rest, done = t.proven);
720
934
  }
721
935
  return done;
722
936
  };
723
937
  // The claims alone, with the facts at the terms they read and then at those the facts read; their constants as
724
- // variables; companions beside them by analogy, then every bound.
938
+ // variables; companions beside them by analogy, then every bound; then ranks of degree 2, over each product of two
939
+ // variables the claims read (up(i, n) = up(i + 1, n) while i < n·n: n·n − i).
940
+ const quads = productsIn(given.map(n => [n.hyps, n.concl])).filter(([a, b]) => given.some(n => n.vars.includes(a) && n.vars.includes(b)));
725
941
  const tries = [
726
942
  [depth, () => given], [depth + 1, () => given],
727
943
  ...(opts.generalize === false ? [] : [
@@ -729,16 +945,17 @@ export function findCycles(facts, claims, opts = {}) {
729
945
  [depth + 1, () => [...given, ...beside(cx, facts, given, big, until, true)]],
730
946
  [depth + 1, () => [...given, ...beside(cx, facts, given, big, until, false)]],
731
947
  ]),
948
+ ...quads.flatMap(p => [[depth, () => given, p], [depth + 1, () => given, p]]),
732
949
  ];
733
950
  const tried = new Set();
734
- for (const [d, t] of tries) {
951
+ for (const [d, t, quad] of tries) {
735
952
  if (performance.now() >= until)
736
953
  break;
737
954
  const nodes = t();
738
- if (tried.has(id([d, nodes.map(n => n.name)])))
955
+ if (tried.has(id([d, nodes.map(n => n.name), quad])))
739
956
  continue;
740
- tried.add(id([d, nodes.map(n => n.name)]));
741
- const out = attempt(nodes, d);
957
+ tried.add(id([d, nodes.map(n => n.name), quad]));
958
+ const out = attempt(nodes, d, quad);
742
959
  if (out.proven || out.stuck === 'sat' && out.why)
743
960
  return out;
744
961
  last = out;
@@ -803,38 +1020,168 @@ export function findRank(l, laws = [], opts = {}) {
803
1020
  }
804
1021
  }
805
1022
  const samples = [[...points.values()].map(env => ({ env, calls: new Set() }))];
806
- const { comps } = lexRanks([0], [node], samples, [...edges.values()], shared, until);
807
- if (!comps.length)
808
- return { proven: false, checked: false, why: `no linear rank, in order, drops on every turn of ${edges.size} sampled` };
809
- const rank = comps.map(c => compE(c, 0, node, shared)), text = `(${rank.map(showE).join(', ')})`;
810
- const first = withDeadline(until, () => goalsProven(loopObligations(l, laws, rank)));
811
- if ('trees' in first)
812
- return { proven: true, checked: true, rank, laws: [], found: { rank: text, laws: [] }, trees: first.trees };
813
- // Invariants the rank needs: the descent as the loop's step laws, inferLaws's candidates chosen by its counterexamples.
814
- const m = loopMachine(l), { stay, cover, helps } = livenessParts(m, loopLiveness(l, rank));
1023
+ const { comps, left } = lexRanks([0], [node], samples, [...edges.values()], shared, until);
1024
+ // Ranks of degree 2 where the linear one leaves turns or the kernel refuses it (while i < N·M: N·M − i, where the runs
1025
+ // alone give 299 − i): one product of two variables at a time. The products the loop's own terms read first (a rank
1026
+ // of degree 2 measures what its terms of degree 2 bound), before the invariants; the others after them, within a
1027
+ // quarter of the time.
1028
+ const read = productsIn([l.while, l.body, l.state, l.pre ?? yes, l.assume ?? yes]).filter(([x, y]) => names.includes(x) && names.includes(y));
1029
+ const rest = products(names).filter(p => !read.some(r => id(r) === id(p)));
1030
+ const seen = new Set();
1031
+ const quadratic = (ps, until) => {
1032
+ const out = [];
1033
+ for (const p of ps) {
1034
+ if (performance.now() > until)
1035
+ break;
1036
+ const d = withProducts([node], samples, [...edges.values()], [p]), r = lexRanks([0], d.nodes, d.samples, d.edges, shared, until);
1037
+ if (!r.comps.length || r.left)
1038
+ continue;
1039
+ const r2 = r.comps.map(c => compE(c, 0, d.nodes[0], shared));
1040
+ if (seen.has(id(r2)))
1041
+ continue;
1042
+ seen.add(id(r2));
1043
+ const verdict = withDeadline(until, () => goalsProven(loopObligations(l, laws, r2)));
1044
+ if (trace)
1045
+ console.error(`[cycles] ${l.name}: degree 2 with ${featName(p)}: (${r2.map(showE).join(', ')}) ${'trees' in verdict ? 'proven' : verdict.goal}`);
1046
+ out.push({ rank: r2, verdict });
1047
+ if ('trees' in verdict)
1048
+ break;
1049
+ }
1050
+ return out;
1051
+ };
1052
+ const done = (rank, trees, more = [], texts = []) => ({ proven: true, checked: true, rank, laws: more, found: { rank: `(${rank.map(showE).join(', ')})`, laws: texts }, trees });
1053
+ // The linear rank first, then those of degree 2 over the products the loop reads, each checked in turn.
1054
+ const linear = comps.map(c => compE(c, 0, node, shared));
1055
+ let tried = [];
1056
+ if (comps.length && !left) {
1057
+ seen.add(id(linear));
1058
+ tried.push({ rank: linear, verdict: withDeadline(until, () => goalsProven(loopObligations(l, laws, linear))) });
1059
+ }
1060
+ tried.push(...quadratic(read, until));
1061
+ const won = tried.find(t => 'trees' in t.verdict);
1062
+ if (won && 'trees' in won.verdict)
1063
+ return done(won.rank, won.verdict.trees);
1064
+ if (!tried.length && comps.length)
1065
+ tried.push({ rank: linear, verdict: withDeadline(until, () => goalsProven(loopObligations(l, laws, linear))) });
1066
+ // Invariants the rank needs: the descent as the loop's step laws, inferLaws's candidates chosen by its counterexamples
1067
+ // (within half the time left where products remain to be tried after them).
1068
+ const base = tried[0];
1069
+ if (base && !('trees' in base.verdict)) {
1070
+ const rank = base.rank, m = loopMachine(l), { stay, cover, helps } = livenessParts(m, loopLiveness(l, rank));
1071
+ const ms = (until - performance.now()) * (rest.length ? 0.5 : 1);
1072
+ if (ms > 300) {
1073
+ const inf = inferLaws(m, laws, [stay, cover, ...helps], { ms });
1074
+ const kept = inf.found.filter(f => !f.law.forall && !f.law.keys?.length), more = kept.map(f => f.law);
1075
+ if (inf.proven && more.length) {
1076
+ const all = [...laws, ...more], again = withDeadline(until + 5000, () => goalsProven(loopObligations(l, all, rank)));
1077
+ if ('trees' in again)
1078
+ return done(rank, again.trees, more, kept.map(f => f.text));
1079
+ }
1080
+ }
1081
+ }
1082
+ // (the products the loop doesn't read within a quarter of its time: a search that fails tries each of them)
1083
+ tried = quadratic(rest, Math.min(until, performance.now() + (opts.ms ?? 10_000) / 4));
1084
+ const late = tried.find(t => 'trees' in t.verdict);
1085
+ if (late && 'trees' in late.verdict)
1086
+ return done(late.rank, late.verdict.trees);
1087
+ if (!base || 'trees' in base.verdict)
1088
+ return { proven: false, checked: false, why: `no rank, linear or of degree 2, in order, drops on every turn of ${edges.size} sampled` };
1089
+ const text = `(${base.rank.map(showE).join(', ')})`, first = base.verdict;
1090
+ return { proven: false, checked: false, rank: base.rank, found: { rank: text, laws: [] }, goal: first.goal, stuck: first.stuck, ...(first.witness ? { witness: first.witness } : {}), why: `rank ${text} from the runs; no invariant found that it needs` };
1091
+ }
1092
+ export function findLeadsTo(m, laws, l, sample, opts = {}) {
1093
+ const t0 = performance.now(), until = t0 + (opts.ms ?? 10_000), rand = seeded(29), run = interpret(m);
1094
+ const names = [...Object.keys(m.state), ...m.params].sort(), helpful = livenessHelpful(l);
1095
+ const node = { name: l.name, hyps: [], concl: yes, vars: names, level: 0, given: true };
1096
+ const env = (ps, st) => ({ ...ps, ...st.scalars });
1097
+ const holds = (c, ps, st) => { try {
1098
+ return evalMC(c, env(ps, st), st, m);
1099
+ }
1100
+ catch {
1101
+ return undefined;
1102
+ } };
1103
+ const pending = (ps, st) => holds(l.from, ps, st) === true && holds(l.to, ps, st) === false;
1104
+ const points = new Map(), strong = new Map(), weak = new Map();
1105
+ for (const bound of [2, 4, 8, 16])
1106
+ for (let trial = 0; trial < 20 && performance.now() < t0 + 1000; trial++) {
1107
+ let ps;
1108
+ try {
1109
+ ps = sample.params(rand, bound);
1110
+ }
1111
+ catch {
1112
+ continue;
1113
+ }
1114
+ if (m.pre && holds(m.pre, ps, { scalars: {}, maps: {} }) !== true)
1115
+ continue;
1116
+ let st;
1117
+ try {
1118
+ st = run.init(ps);
1119
+ }
1120
+ catch {
1121
+ continue;
1122
+ }
1123
+ for (let turn = 0; turn < 120; turn++) {
1124
+ const here = env(ps, st), was = pending(ps, st);
1125
+ if (was)
1126
+ points.set(id(here), here);
1127
+ // A helpful request in its region half the time (its named inputs at the state, the rest drawn), else any.
1128
+ const regions = helpful.filter(h => holds(h.when, ps, st) === true), h = regions.length && rand(2) ? regions[rand(regions.length)] : undefined;
1129
+ let next;
1130
+ for (let tries = 0; tries < 20 && !next; tries++) {
1131
+ try {
1132
+ const input = sample.input(rand, bound, ps);
1133
+ if (h)
1134
+ for (const [x, t] of Object.entries(h.request))
1135
+ input[x] = evalME(t, here, st, m);
1136
+ next = run.step(ps, st, input);
1137
+ }
1138
+ catch { /* refused: another */ }
1139
+ }
1140
+ if (!next)
1141
+ break;
1142
+ if (was && pending(ps, next)) {
1143
+ const e = { from: 0, to: 0, s: here, t: env(ps, next) };
1144
+ (h ? strong : weak).set(id([e.s, e.t]), e);
1145
+ }
1146
+ st = next;
1147
+ }
1148
+ }
1149
+ const samples = [[...points.values()].map(env => ({ env, calls: new Set() }))];
1150
+ const { comps, left } = lexRanks([0], [node], samples, [...strong.values()], shared, until, false, [...weak.values()]);
1151
+ if (!comps.length || left)
1152
+ return { proven: false, checked: false, why: `no linear rank, in order, that ${strong.size} helpful steps sampled lower and ${weak.size} others keep` };
1153
+ const rank = comps.map(c => compE(c, 0, node, shared)), text = `(${rank.map(showE).join(', ')})`, lv = { ...l, rank };
1154
+ const first = withDeadline(until, () => proveLiveness(m, laws, lv));
1155
+ if (first.proven && first.checked)
1156
+ return { proven: true, checked: true, rank, laws: [], found: { rank: text, laws: [] } };
815
1157
  const ms = until - performance.now();
816
1158
  if (ms > 300) {
817
- const inf = inferLaws(m, laws, [stay, cover, ...helps], { ms });
1159
+ const { stay, cover, helps } = livenessParts(m, lv), inf = inferLaws(m, laws, [stay, cover, ...helps], { ms });
818
1160
  const kept = inf.found.filter(f => !f.law.forall && !f.law.keys?.length), more = kept.map(f => f.law);
819
1161
  if (inf.proven && more.length) {
820
- const all = [...laws, ...more], again = withDeadline(until + 5000, () => goalsProven(loopObligations(l, all, rank)));
821
- if ('trees' in again)
822
- return { proven: true, checked: true, rank, laws: more, found: { rank: text, laws: kept.map(f => f.text) }, trees: again.trees };
1162
+ const again = withDeadline(until + 5000, () => proveLiveness(m, [...laws, ...more], lv));
1163
+ if (again.proven && again.checked)
1164
+ return { proven: true, checked: true, rank, laws: more, found: { rank: text, laws: kept.map(f => f.text) } };
823
1165
  }
824
1166
  }
825
- return { proven: false, checked: false, rank, found: { rank: text, laws: [] }, goal: first.goal, stuck: first.stuck, ...(first.witness ? { witness: first.witness } : {}), why: `rank ${text} from the runs; no invariant found that it needs` };
1167
+ return { proven: false, checked: false, rank, found: { rank: text, laws: [] }, ...(first.failed ? { goal: first.failed.goal, stuck: first.failed.stuck, ...(first.failed.witness ? { witness: first.failed.witness } : {}) } : {}), why: `rank ${text} from the runs; the kernel's goals not proven with it` };
826
1168
  }
827
1169
  export function cycleLemmas(m, laws, stepLaws = [], opts = {}) {
828
1170
  const until = performance.now() + (opts.ms ?? 10_000);
829
1171
  const fns = Object.entries(m.fns ?? {}).filter(([, f]) => !f.recur && (f.arity ?? 1) === 1);
830
- if (!fns.length || !m.facts?.length)
1172
+ const fns2 = Object.entries(m.fns ?? {}).filter(([, f]) => !f.recur && f.arity === 2 && f.call);
1173
+ if ((!fns.length && !fns2.length) || !m.facts?.length)
831
1174
  return { proven: false, checked: false, found: [], why: 'no function known by its facts' };
832
1175
  const facts = m.facts.map(f => ({ name: f.name, keys: [...(f.forall ? [f.forall] : []), ...(f.keys ?? [])], holds: f.holds }));
833
1176
  const values = {};
834
1177
  for (const [name, f] of fns)
835
1178
  values[name] = (q) => fnValue(f, q);
1179
+ for (const [name, f] of fns2) {
1180
+ const g = compileFunction([], `return (${f.call})`)();
1181
+ values[name] = (a, b) => g(a, b);
1182
+ }
836
1183
  const ts = [...new Set(constsOf([m, laws, stepLaws]).filter(c => c >= 0 && c <= 1000).flatMap(c => [c - 1, c, c + 1]).filter(c => c >= 0))].sort((a, b) => a - b);
837
- const big = Math.max(24, ...ts), x = 'k$', claims = [];
1184
+ const big = Math.max(24, ...ts), x = 'k$', y = 'j$', claims = [];
838
1185
  for (const [name] of fns) {
839
1186
  const f = values[name];
840
1187
  if (!f)
@@ -872,14 +1219,72 @@ export function cycleLemmas(m, laws, stepLaws = [], opts = {}) {
872
1219
  if (vals.slice(vals.length >> 1).includes(top))
873
1220
  claims.push({ name: `${name}(k) ≤ ${top}`, concl: le(read, k(top)) });
874
1221
  }
875
- const unique = [...new Map(claims.map(c => [c.name, c])).values()];
876
- if (!unique.length)
877
- return { proven: false, checked: false, found: [], why: 'no lemma fits the values' };
878
- const out = findCycles(facts, unique, { ms: Math.max(0, until - performance.now()) * 0.6, values, houdini: true, generalize: false });
879
- if (!out.proven || !out.checked || !out.kept?.length)
1222
+ // A function of two arguments: over a grid of both, the line that fits exactly, and each bound by 0, either argument or
1223
+ // their sum plus the tightest constant (above, and below where it is one): a constant above only where the values
1224
+ // reach it below half the grid in both arguments (not the grid's own size).
1225
+ for (const [name] of fns2) {
1226
+ const f = values[name], B = Math.min(big, 24), pts = [];
1227
+ for (let a = 0; a <= B; a++)
1228
+ for (let b = 0; b <= B; b++) {
1229
+ let z;
1230
+ try {
1231
+ z = f(a, b);
1232
+ }
1233
+ catch {
1234
+ continue;
1235
+ }
1236
+ if (Number.isSafeInteger(z) && z >= 0)
1237
+ pts.push({ env: { [x]: a, [y]: b }, value: z });
1238
+ }
1239
+ if (pts.length < 16)
1240
+ continue;
1241
+ const read = get(name, argsKey([v(x), v(y)])), show = (l, c) => showE(linE(Object.fromEntries(Object.entries(l).map(([n, q]) => [n === x ? 'k' : 'j', q])), c));
1242
+ const line = fitExact([x, y], pts, until);
1243
+ if (line) {
1244
+ claims.push({ name: `${name}(k, j) = ${show(line.coef, line.c)}`, concl: eqC(read, linE(line.coef, line.c)) });
1245
+ continue;
1246
+ }
1247
+ for (const form of [{}, { [x]: 1 }, { [y]: 1 }, { [x]: 1, [y]: 1 }]) {
1248
+ const at = (p) => Object.entries(form).reduce((acc, [n, q]) => acc + q * p.env[n], 0);
1249
+ const d = Math.max(...pts.map(p => p.value - at(p))), lo = Math.min(...pts.map(p => p.value - at(p)));
1250
+ const reached = pts.some(p => p.value - at(p) === d && p.env[x] <= B / 2 && p.env[y] <= B / 2);
1251
+ if (Object.keys(form).length ? d <= 0 || reached : reached)
1252
+ claims.push({ name: `${name}(k, j) ≤ ${show(form, d)}`, concl: d >= 0 ? le(read, linE(form, d)) : le(add(read, k(-d)), linE(form, 0)) });
1253
+ if (Object.keys(form).length && lo >= 0)
1254
+ claims.push({ name: `${name}(k, j) ≥ ${show(form, lo)}`, concl: le(linE(form, lo), read) });
1255
+ }
1256
+ }
1257
+ // Those of two arguments each proven alone (their keys are pairings, products: a goal that reads several such claims
1258
+ // at once is slow to refute), the rest together; the proof the nodes of each, side by side (no node of one uses
1259
+ // another's).
1260
+ const unique = [...new Map(claims.map(c => [c.name, c])).values()], two = (c) => m.fns?.[readsOf(c.concl)[0]?.map ?? '']?.arity === 2;
1261
+ const proofs = new Map();
1262
+ for (const c of unique.filter(two)) {
1263
+ if (performance.now() >= until)
1264
+ break;
1265
+ const o = findCycles(facts, [c], { ms: Math.min(1000, (until - performance.now()) * 0.1), values, houdini: true, generalize: false });
1266
+ if (o.proven && o.checked && o.proof)
1267
+ proofs.set(c.name, o.proof);
1268
+ }
1269
+ const ones = unique.filter(c => !two(c));
1270
+ if (!ones.length && !proofs.size)
1271
+ return { proven: false, checked: false, found: [], why: unique.length ? 'no lemma proven' : 'no lemma fits the values' };
1272
+ const out = ones.length ? findCycles(facts, ones, { ms: Math.max(0, until - performance.now()) * 0.6, values, houdini: true, generalize: false }) : { proven: true, checked: true, kept: [] };
1273
+ if ((!out.proven || !out.checked || !out.kept?.length) && !proofs.size)
880
1274
  return { proven: false, checked: false, found: [], why: out.why ?? 'no lemma proven' };
881
- const asLaw = (c) => ({ law: { name: c.name, forall: x, holds: c.hyps?.length ? { op: 'or', xs: [{ op: 'not', x: and(...c.hyps) }, c.concl] } : c.concl }, text: c.name });
882
- let found = unique.filter(c => out.kept.includes(c.name)).map(asLaw);
1275
+ const kept = [...(out.proven && out.checked ? out.kept ?? [] : []), ...proofs.keys()];
1276
+ const sideBySide = (ps) => {
1277
+ if (!ps.length)
1278
+ return undefined;
1279
+ let at = 0;
1280
+ const nodes = ps.flatMap(p => { const off = at; at += p.nodes.length; return p.nodes.map(n => ({ ...n, uses: n.uses.map(u => ({ ...u, node: u.node + off })) })); });
1281
+ return { facts: ps[0].facts, nodes };
1282
+ };
1283
+ const asLaw = (c) => {
1284
+ const holds = c.hyps?.length ? { op: 'or', xs: [{ op: 'not', x: and(...c.hyps) }, c.concl] } : c.concl;
1285
+ return { law: { name: c.name, forall: x, ...(varsOf(holds).includes(y) ? { keys: [y] } : {}), holds }, text: c.name };
1286
+ };
1287
+ let found = unique.filter(c => kept.includes(c.name)).map(asLaw);
883
1288
  const proveWith = (fs) => {
884
1289
  const mm = { ...m, facts: [...m.facts, ...fs.map(f => f.law)] };
885
1290
  const r = withDeadline(until, () => (stepLaws.length ? proveStepLaws(mm, laws, stepLaws) : proveLaws(mm, laws)));
@@ -895,5 +1300,7 @@ export function cycleLemmas(m, laws, stepLaws = [], opts = {}) {
895
1300
  if (again.ok)
896
1301
  (found = rest, best = again);
897
1302
  }
898
- return { proven: true, checked: true, machine: best.machine, found, ...(out.proof ? { proof: out.proof } : {}) };
1303
+ const merged = sideBySide([...(out.proof && found.some(f => ones.some(c => c.name === f.text)) ? [out.proof] : []), ...found.flatMap(f => (proofs.has(f.text) ? [proofs.get(f.text)] : []))]);
1304
+ const proof = merged && checkCyclic(merged) ? merged : undefined;
1305
+ return { proven: true, checked: true, machine: best.machine, found, ...(proof ? { proof } : {}) };
899
1306
  }