gdp-wired 0.18.0 → 0.20.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,44 @@ 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
+ 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));
629
+ // 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
630
+ // each sample the product's, one product at a time (a sparse template: with every product at once, the runs fit a rank
631
+ // of many small parts that no turn past them keeps). The rank found is linear in it, the product read back as the term
632
+ // 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
633
+ // products of rows and squares, Positivstellensatz of degree 2, where they need more).
634
+ const featSep = '·';
635
+ const products = (names) => names.flatMap((a, i) => names.slice(i).map(b => [a, b]));
636
+ const featName = ([a, b]) => `${a}${featSep}${b}`;
637
+ const withFeat = (env, p) => (p && env[p[0]] !== undefined && env[p[1]] !== undefined ? { ...env, [featName(p)]: env[p[0]] * env[p[1]] } : env);
638
+ // (linE builds sums, differences and multiples of variables and constants only)
639
+ const unfeat = (e) => {
640
+ if (e.op === 'var') {
641
+ const [a, b] = e.name.split(featSep);
642
+ return b === undefined ? e : { op: 'prod', a: v(a), b: v(b) };
643
+ }
644
+ if (e.op === 'add' || e.op === 'sub')
645
+ return { ...e, a: unfeat(e.a), b: unfeat(e.b) };
646
+ return e.op === 'mul' ? { ...e, a: unfeat(e.a) } : e;
647
+ };
648
+ // The products of two variables a term reads, each as its sorted pair.
649
+ const productsIn = (t) => {
650
+ const out = new Map();
651
+ walk(t, o => { if (o.op === 'prod') {
652
+ const a = o.a, b = o.b;
653
+ if (a.op === 'var' && b.op === 'var') {
654
+ const p = [a.name, b.name].sort();
655
+ out.set(id(p), p);
656
+ }
657
+ } });
658
+ return [...out.values()];
659
+ };
660
+ // The nodes, samples and edges with one product of two variables per node a variable (none where `ps[i]` is undefined).
661
+ const withProducts = (nodes, samples, edges, ps) => ({
662
+ nodes: nodes.map((n, i) => (ps[i] ? { ...n, vars: [...n.vars, featName(ps[i])] } : n)),
663
+ samples: samples.map((ss, i) => ss.map(s => ({ ...s, env: withFeat(s.env, ps[i]) }))),
664
+ edges: edges.map(e => ({ ...e, s: withFeat(e.s, ps[e.from]), t: withFeat(e.t, ps[e.to]) })),
665
+ });
462
666
  // Strongly connected components (Tarjan), each node's component's index.
463
667
  function sccs(graph) {
464
668
  const index = [], low = [], on = [], stack = [], comp = [];
@@ -490,7 +694,8 @@ function sccs(graph) {
490
694
  visit(u);
491
695
  return comp;
492
696
  }
493
- function build(facts, cx, nodes, depth, big, until) {
697
+ // `quad`: ranks of degree 2, the product of those two variables a further one of each node that has both.
698
+ function build(facts, cx, nodes, depth, big, until, quad) {
494
699
  const inst = nodes.map(n => instancesFor(facts, cx, n, depth));
495
700
  const samples = [];
496
701
  for (const n of nodes) {
@@ -539,21 +744,23 @@ function build(facts, cx, nodes, depth, big, until) {
539
744
  parts.set(i, []);
540
745
  continue;
541
746
  }
542
- let best = { ...lexRanks(idx, nodes, samples, within, shared, until), name: shared };
747
+ 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 };
748
+ let best = { ...lexRanks(idx, q.nodes, q.samples, q.edges, shared, until, !!quad), name: shared };
543
749
  if (best.left && idx.length > 1 && idx.length <= 4) {
544
- const o = lexRanks(idx, nodes, samples, within, own(nodes), until);
750
+ const o = lexRanks(idx, q.nodes, q.samples, q.edges, own(q.nodes), until, !!quad);
545
751
  if (o.left < best.left)
546
- best = { ...o, name: own(nodes) };
752
+ best = { ...o, name: own(q.nodes) };
547
753
  }
548
754
  for (const i of idx)
549
- parts.set(i, best.comps.map(cp => compE(cp, i, nodes[i], best.name)));
755
+ parts.set(i, best.comps.map(cp => compE(cp, i, q.nodes[i], best.name)));
550
756
  }
551
757
  const width = Math.max(0, ...[...parts.values()].map(p => p.length)), tiers = graph.some(g => g.length);
552
758
  const ranks = nodes.map((_, i) => [...(tiers ? [k(heightOf(comp[i]))] : []), ...parts.get(i), ...Array.from({ length: width - parts.get(i).length }, () => k(0))]);
553
759
  let uses = 0;
554
760
  const specs = nodes.map((n, i) => ({
555
761
  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 => {
762
+ // (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)
763
+ 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
764
  uses++;
558
765
  const m = nodes[c.node], s = vs(c.subst);
559
766
  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 +879,8 @@ export function findCycles(facts, claims, opts = {}) {
672
879
  const big = Math.max(0, ...constsOf([facts.map(f => f.holds), claims]).filter(c => c <= 1000));
673
880
  const given = claims.map(c => nodeOf(c, 1, !opts.houdini));
674
881
  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));
882
+ const once = (nodes, depth, quad) => {
883
+ const b = withDeadline(until, () => build(facts, cx, nodes, depth, big, until, quad));
677
884
  if ('broken' in b) {
678
885
  const at = nodes.find(n => n.name === b.broken.node);
679
886
  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 +902,10 @@ export function findCycles(facts, claims, opts = {}) {
695
902
  };
696
903
  // Houdini over the nodes that may be dropped (each that fails, dropped, the rest proven again); then each companion
697
904
  // dropped in turn where the rest are proven without it (only what the claims need is kept).
698
- const attempt = (start, depth) => {
905
+ const attempt = (start, depth, quad) => {
699
906
  let nodes = start, done;
700
907
  for (let round = 0; round <= start.length && nodes.length && performance.now() < until && !done; round++) {
701
- const t = once(nodes, depth);
908
+ const t = once(nodes, depth, quad);
702
909
  if ('proven' in t) {
703
910
  done = t.proven;
704
911
  break;
@@ -714,14 +921,16 @@ export function findCycles(facts, claims, opts = {}) {
714
921
  for (const c of nodes.filter(n => !n.given)) {
715
922
  if (performance.now() >= until)
716
923
  break;
717
- const rest = nodes.filter(n => n !== c), t = once(rest, depth);
924
+ const rest = nodes.filter(n => n !== c), t = once(rest, depth, quad);
718
925
  if ('proven' in t && t.proven.checked)
719
926
  (nodes = rest, done = t.proven);
720
927
  }
721
928
  return done;
722
929
  };
723
930
  // 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.
931
+ // variables; companions beside them by analogy, then every bound; then ranks of degree 2, over each product of two
932
+ // variables the claims read (up(i, n) = up(i + 1, n) while i < n·n: n·n − i).
933
+ 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
934
  const tries = [
726
935
  [depth, () => given], [depth + 1, () => given],
727
936
  ...(opts.generalize === false ? [] : [
@@ -729,16 +938,17 @@ export function findCycles(facts, claims, opts = {}) {
729
938
  [depth + 1, () => [...given, ...beside(cx, facts, given, big, until, true)]],
730
939
  [depth + 1, () => [...given, ...beside(cx, facts, given, big, until, false)]],
731
940
  ]),
941
+ ...quads.flatMap(p => [[depth, () => given, p], [depth + 1, () => given, p]]),
732
942
  ];
733
943
  const tried = new Set();
734
- for (const [d, t] of tries) {
944
+ for (const [d, t, quad] of tries) {
735
945
  if (performance.now() >= until)
736
946
  break;
737
947
  const nodes = t();
738
- if (tried.has(id([d, nodes.map(n => n.name)])))
948
+ if (tried.has(id([d, nodes.map(n => n.name), quad])))
739
949
  continue;
740
- tried.add(id([d, nodes.map(n => n.name)]));
741
- const out = attempt(nodes, d);
950
+ tried.add(id([d, nodes.map(n => n.name), quad]));
951
+ const out = attempt(nodes, d, quad);
742
952
  if (out.proven || out.stuck === 'sat' && out.why)
743
953
  return out;
744
954
  last = out;
@@ -803,38 +1013,168 @@ export function findRank(l, laws = [], opts = {}) {
803
1013
  }
804
1014
  }
805
1015
  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));
1016
+ const { comps, left } = lexRanks([0], [node], samples, [...edges.values()], shared, until);
1017
+ // 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
1018
+ // alone give 299 − i): one product of two variables at a time. The products the loop's own terms read first (a rank
1019
+ // of degree 2 measures what its terms of degree 2 bound), before the invariants; the others after them, within a
1020
+ // quarter of the time.
1021
+ const read = productsIn([l.while, l.body, l.state, l.pre ?? yes, l.assume ?? yes]).filter(([x, y]) => names.includes(x) && names.includes(y));
1022
+ const rest = products(names).filter(p => !read.some(r => id(r) === id(p)));
1023
+ const seen = new Set();
1024
+ const quadratic = (ps, until) => {
1025
+ const out = [];
1026
+ for (const p of ps) {
1027
+ if (performance.now() > until)
1028
+ break;
1029
+ const d = withProducts([node], samples, [...edges.values()], [p]), r = lexRanks([0], d.nodes, d.samples, d.edges, shared, until);
1030
+ if (!r.comps.length || r.left)
1031
+ continue;
1032
+ const r2 = r.comps.map(c => compE(c, 0, d.nodes[0], shared));
1033
+ if (seen.has(id(r2)))
1034
+ continue;
1035
+ seen.add(id(r2));
1036
+ const verdict = withDeadline(until, () => goalsProven(loopObligations(l, laws, r2)));
1037
+ if (trace)
1038
+ console.error(`[cycles] ${l.name}: degree 2 with ${featName(p)}: (${r2.map(showE).join(', ')}) ${'trees' in verdict ? 'proven' : verdict.goal}`);
1039
+ out.push({ rank: r2, verdict });
1040
+ if ('trees' in verdict)
1041
+ break;
1042
+ }
1043
+ return out;
1044
+ };
1045
+ const done = (rank, trees, more = [], texts = []) => ({ proven: true, checked: true, rank, laws: more, found: { rank: `(${rank.map(showE).join(', ')})`, laws: texts }, trees });
1046
+ // The linear rank first, then those of degree 2 over the products the loop reads, each checked in turn.
1047
+ const linear = comps.map(c => compE(c, 0, node, shared));
1048
+ let tried = [];
1049
+ if (comps.length && !left) {
1050
+ seen.add(id(linear));
1051
+ tried.push({ rank: linear, verdict: withDeadline(until, () => goalsProven(loopObligations(l, laws, linear))) });
1052
+ }
1053
+ tried.push(...quadratic(read, until));
1054
+ const won = tried.find(t => 'trees' in t.verdict);
1055
+ if (won && 'trees' in won.verdict)
1056
+ return done(won.rank, won.verdict.trees);
1057
+ if (!tried.length && comps.length)
1058
+ tried.push({ rank: linear, verdict: withDeadline(until, () => goalsProven(loopObligations(l, laws, linear))) });
1059
+ // Invariants the rank needs: the descent as the loop's step laws, inferLaws's candidates chosen by its counterexamples
1060
+ // (within half the time left where products remain to be tried after them).
1061
+ const base = tried[0];
1062
+ if (base && !('trees' in base.verdict)) {
1063
+ const rank = base.rank, m = loopMachine(l), { stay, cover, helps } = livenessParts(m, loopLiveness(l, rank));
1064
+ const ms = (until - performance.now()) * (rest.length ? 0.5 : 1);
1065
+ if (ms > 300) {
1066
+ const inf = inferLaws(m, laws, [stay, cover, ...helps], { ms });
1067
+ const kept = inf.found.filter(f => !f.law.forall && !f.law.keys?.length), more = kept.map(f => f.law);
1068
+ if (inf.proven && more.length) {
1069
+ const all = [...laws, ...more], again = withDeadline(until + 5000, () => goalsProven(loopObligations(l, all, rank)));
1070
+ if ('trees' in again)
1071
+ return done(rank, again.trees, more, kept.map(f => f.text));
1072
+ }
1073
+ }
1074
+ }
1075
+ // (the products the loop doesn't read within a quarter of its time: a search that fails tries each of them)
1076
+ tried = quadratic(rest, Math.min(until, performance.now() + (opts.ms ?? 10_000) / 4));
1077
+ const late = tried.find(t => 'trees' in t.verdict);
1078
+ if (late && 'trees' in late.verdict)
1079
+ return done(late.rank, late.verdict.trees);
1080
+ if (!base || 'trees' in base.verdict)
1081
+ return { proven: false, checked: false, why: `no rank, linear or of degree 2, in order, drops on every turn of ${edges.size} sampled` };
1082
+ const text = `(${base.rank.map(showE).join(', ')})`, first = base.verdict;
1083
+ 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` };
1084
+ }
1085
+ export function findLeadsTo(m, laws, l, sample, opts = {}) {
1086
+ const t0 = performance.now(), until = t0 + (opts.ms ?? 10_000), rand = seeded(29), run = interpret(m);
1087
+ const names = [...Object.keys(m.state), ...m.params].sort(), helpful = livenessHelpful(l);
1088
+ const node = { name: l.name, hyps: [], concl: yes, vars: names, level: 0, given: true };
1089
+ const env = (ps, st) => ({ ...ps, ...st.scalars });
1090
+ const holds = (c, ps, st) => { try {
1091
+ return evalMC(c, env(ps, st), st, m);
1092
+ }
1093
+ catch {
1094
+ return undefined;
1095
+ } };
1096
+ const pending = (ps, st) => holds(l.from, ps, st) === true && holds(l.to, ps, st) === false;
1097
+ const points = new Map(), strong = new Map(), weak = new Map();
1098
+ for (const bound of [2, 4, 8, 16])
1099
+ for (let trial = 0; trial < 20 && performance.now() < t0 + 1000; trial++) {
1100
+ let ps;
1101
+ try {
1102
+ ps = sample.params(rand, bound);
1103
+ }
1104
+ catch {
1105
+ continue;
1106
+ }
1107
+ if (m.pre && holds(m.pre, ps, { scalars: {}, maps: {} }) !== true)
1108
+ continue;
1109
+ let st;
1110
+ try {
1111
+ st = run.init(ps);
1112
+ }
1113
+ catch {
1114
+ continue;
1115
+ }
1116
+ for (let turn = 0; turn < 120; turn++) {
1117
+ const here = env(ps, st), was = pending(ps, st);
1118
+ if (was)
1119
+ points.set(id(here), here);
1120
+ // A helpful request in its region half the time (its named inputs at the state, the rest drawn), else any.
1121
+ const regions = helpful.filter(h => holds(h.when, ps, st) === true), h = regions.length && rand(2) ? regions[rand(regions.length)] : undefined;
1122
+ let next;
1123
+ for (let tries = 0; tries < 20 && !next; tries++) {
1124
+ try {
1125
+ const input = sample.input(rand, bound, ps);
1126
+ if (h)
1127
+ for (const [x, t] of Object.entries(h.request))
1128
+ input[x] = evalME(t, here, st, m);
1129
+ next = run.step(ps, st, input);
1130
+ }
1131
+ catch { /* refused: another */ }
1132
+ }
1133
+ if (!next)
1134
+ break;
1135
+ if (was && pending(ps, next)) {
1136
+ const e = { from: 0, to: 0, s: here, t: env(ps, next) };
1137
+ (h ? strong : weak).set(id([e.s, e.t]), e);
1138
+ }
1139
+ st = next;
1140
+ }
1141
+ }
1142
+ const samples = [[...points.values()].map(env => ({ env, calls: new Set() }))];
1143
+ const { comps, left } = lexRanks([0], [node], samples, [...strong.values()], shared, until, false, [...weak.values()]);
1144
+ if (!comps.length || left)
1145
+ return { proven: false, checked: false, why: `no linear rank, in order, that ${strong.size} helpful steps sampled lower and ${weak.size} others keep` };
1146
+ const rank = comps.map(c => compE(c, 0, node, shared)), text = `(${rank.map(showE).join(', ')})`, lv = { ...l, rank };
1147
+ const first = withDeadline(until, () => proveLiveness(m, laws, lv));
1148
+ if (first.proven && first.checked)
1149
+ return { proven: true, checked: true, rank, laws: [], found: { rank: text, laws: [] } };
815
1150
  const ms = until - performance.now();
816
1151
  if (ms > 300) {
817
- const inf = inferLaws(m, laws, [stay, cover, ...helps], { ms });
1152
+ const { stay, cover, helps } = livenessParts(m, lv), inf = inferLaws(m, laws, [stay, cover, ...helps], { ms });
818
1153
  const kept = inf.found.filter(f => !f.law.forall && !f.law.keys?.length), more = kept.map(f => f.law);
819
1154
  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 };
1155
+ const again = withDeadline(until + 5000, () => proveLiveness(m, [...laws, ...more], lv));
1156
+ if (again.proven && again.checked)
1157
+ return { proven: true, checked: true, rank, laws: more, found: { rank: text, laws: kept.map(f => f.text) } };
823
1158
  }
824
1159
  }
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` };
1160
+ 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
1161
  }
827
1162
  export function cycleLemmas(m, laws, stepLaws = [], opts = {}) {
828
1163
  const until = performance.now() + (opts.ms ?? 10_000);
829
1164
  const fns = Object.entries(m.fns ?? {}).filter(([, f]) => !f.recur && (f.arity ?? 1) === 1);
830
- if (!fns.length || !m.facts?.length)
1165
+ const fns2 = Object.entries(m.fns ?? {}).filter(([, f]) => !f.recur && f.arity === 2 && f.call);
1166
+ if ((!fns.length && !fns2.length) || !m.facts?.length)
831
1167
  return { proven: false, checked: false, found: [], why: 'no function known by its facts' };
832
1168
  const facts = m.facts.map(f => ({ name: f.name, keys: [...(f.forall ? [f.forall] : []), ...(f.keys ?? [])], holds: f.holds }));
833
1169
  const values = {};
834
1170
  for (const [name, f] of fns)
835
1171
  values[name] = (q) => fnValue(f, q);
1172
+ for (const [name, f] of fns2) {
1173
+ const g = compileFunction([], `return (${f.call})`)();
1174
+ values[name] = (a, b) => g(a, b);
1175
+ }
836
1176
  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 = [];
1177
+ const big = Math.max(24, ...ts), x = 'k$', y = 'j$', claims = [];
838
1178
  for (const [name] of fns) {
839
1179
  const f = values[name];
840
1180
  if (!f)
@@ -872,14 +1212,72 @@ export function cycleLemmas(m, laws, stepLaws = [], opts = {}) {
872
1212
  if (vals.slice(vals.length >> 1).includes(top))
873
1213
  claims.push({ name: `${name}(k) ≤ ${top}`, concl: le(read, k(top)) });
874
1214
  }
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)
1215
+ // A function of two arguments: over a grid of both, the line that fits exactly, and each bound by 0, either argument or
1216
+ // their sum plus the tightest constant (above, and below where it is one): a constant above only where the values
1217
+ // reach it below half the grid in both arguments (not the grid's own size).
1218
+ for (const [name] of fns2) {
1219
+ const f = values[name], B = Math.min(big, 24), pts = [];
1220
+ for (let a = 0; a <= B; a++)
1221
+ for (let b = 0; b <= B; b++) {
1222
+ let z;
1223
+ try {
1224
+ z = f(a, b);
1225
+ }
1226
+ catch {
1227
+ continue;
1228
+ }
1229
+ if (Number.isSafeInteger(z) && z >= 0)
1230
+ pts.push({ env: { [x]: a, [y]: b }, value: z });
1231
+ }
1232
+ if (pts.length < 16)
1233
+ continue;
1234
+ 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));
1235
+ const line = fitExact([x, y], pts, until);
1236
+ if (line) {
1237
+ claims.push({ name: `${name}(k, j) = ${show(line.coef, line.c)}`, concl: eqC(read, linE(line.coef, line.c)) });
1238
+ continue;
1239
+ }
1240
+ for (const form of [{}, { [x]: 1 }, { [y]: 1 }, { [x]: 1, [y]: 1 }]) {
1241
+ const at = (p) => Object.entries(form).reduce((acc, [n, q]) => acc + q * p.env[n], 0);
1242
+ const d = Math.max(...pts.map(p => p.value - at(p))), lo = Math.min(...pts.map(p => p.value - at(p)));
1243
+ const reached = pts.some(p => p.value - at(p) === d && p.env[x] <= B / 2 && p.env[y] <= B / 2);
1244
+ if (Object.keys(form).length ? d <= 0 || reached : reached)
1245
+ 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)) });
1246
+ if (Object.keys(form).length && lo >= 0)
1247
+ claims.push({ name: `${name}(k, j) ≥ ${show(form, lo)}`, concl: le(linE(form, lo), read) });
1248
+ }
1249
+ }
1250
+ // Those of two arguments each proven alone (their keys are pairings, products: a goal that reads several such claims
1251
+ // at once is slow to refute), the rest together; the proof the nodes of each, side by side (no node of one uses
1252
+ // another's).
1253
+ const unique = [...new Map(claims.map(c => [c.name, c])).values()], two = (c) => m.fns?.[readsOf(c.concl)[0]?.map ?? '']?.arity === 2;
1254
+ const proofs = new Map();
1255
+ for (const c of unique.filter(two)) {
1256
+ if (performance.now() >= until)
1257
+ break;
1258
+ const o = findCycles(facts, [c], { ms: Math.min(1000, (until - performance.now()) * 0.1), values, houdini: true, generalize: false });
1259
+ if (o.proven && o.checked && o.proof)
1260
+ proofs.set(c.name, o.proof);
1261
+ }
1262
+ const ones = unique.filter(c => !two(c));
1263
+ if (!ones.length && !proofs.size)
1264
+ return { proven: false, checked: false, found: [], why: unique.length ? 'no lemma proven' : 'no lemma fits the values' };
1265
+ 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: [] };
1266
+ if ((!out.proven || !out.checked || !out.kept?.length) && !proofs.size)
880
1267
  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);
1268
+ const kept = [...(out.proven && out.checked ? out.kept ?? [] : []), ...proofs.keys()];
1269
+ const sideBySide = (ps) => {
1270
+ if (!ps.length)
1271
+ return undefined;
1272
+ let at = 0;
1273
+ 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 })) })); });
1274
+ return { facts: ps[0].facts, nodes };
1275
+ };
1276
+ const asLaw = (c) => {
1277
+ const holds = c.hyps?.length ? { op: 'or', xs: [{ op: 'not', x: and(...c.hyps) }, c.concl] } : c.concl;
1278
+ return { law: { name: c.name, forall: x, ...(varsOf(holds).includes(y) ? { keys: [y] } : {}), holds }, text: c.name };
1279
+ };
1280
+ let found = unique.filter(c => kept.includes(c.name)).map(asLaw);
883
1281
  const proveWith = (fs) => {
884
1282
  const mm = { ...m, facts: [...m.facts, ...fs.map(f => f.law)] };
885
1283
  const r = withDeadline(until, () => (stepLaws.length ? proveStepLaws(mm, laws, stepLaws) : proveLaws(mm, laws)));
@@ -895,5 +1293,7 @@ export function cycleLemmas(m, laws, stepLaws = [], opts = {}) {
895
1293
  if (again.ok)
896
1294
  (found = rest, best = again);
897
1295
  }
898
- return { proven: true, checked: true, machine: best.machine, found, ...(out.proof ? { proof: out.proof } : {}) };
1296
+ 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)] : []))]);
1297
+ const proof = merged && checkCyclic(merged) ? merged : undefined;
1298
+ return { proven: true, checked: true, machine: best.machine, found, ...(proof ? { proof } : {}) };
899
1299
  }