gdp-wired 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core/dsl.js CHANGED
@@ -18,7 +18,10 @@
18
18
  import { carriedView, compileFunction, evalC, evalE, interpret, loadJs, numbersOf, pairedArgs } from "./machine.js";
19
19
  import { allowed, keysOf, requestEnv, watch } from "./watch.js";
20
20
  import { check, foldObligations, loopObligations } from "../kernel/kernel.js";
21
- import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws } from "../kernel/prove.js";
21
+ import { factsFor, grow, proveLaws, proveLiveness, proveStepLaws, withDeadline } from "../kernel/prove.js";
22
+ import { crossCyclic, cyclicOn, foldNodes, proveCyclic, quietly, recurNodes } from "./cyclic.js";
23
+ import { discover } from "./discover.js";
24
+ import { inferLaws } from "./infer.js";
22
25
  // --- terms and conditions -------------------------------------------------------------------------------------------
23
26
  const naturalConst = (v) => {
24
27
  if (!Number.isSafeInteger(v) || v < 0)
@@ -860,6 +863,43 @@ export class ListRef {
860
863
  within() { return forall(k => this.len.atMost(k).implies(this.at(k).eq(0))); }
861
864
  increasing() { return forall((i, j) => i.lt(j).and(j.lt(this.len)).implies(this.at(i).lt(this.at(j)))); }
862
865
  }
866
+ // A sequence of naturals as positions and a length (a path's segments, as interned names): `at(i)` its value at i,
867
+ // `len` its length. Comparing two is a condition about every position below a length (the array property fragment,
868
+ // decidable), so a law about prefixes needs no induction. One a request carries (`carriesSeq`), or a row of a table of
869
+ // them (`carriesSeqs`), its positions read from its row's offset in the table's one map.
870
+ export class SeqRef {
871
+ map;
872
+ len;
873
+ base;
874
+ constructor(map, len, base) {
875
+ this.map = map;
876
+ this.len = len;
877
+ this.base = base;
878
+ }
879
+ at(i) { return this.map.at(!this.base ? i : typeof i === 'number' ? this.base.plus(i) : i.plus(this.base)); }
880
+ // The same values at every position below `upTo` (both have them).
881
+ agree(o, upTo) { return everyKey(k => k.lt(upTo).implies(this.at(k).eq(o.at(k)))); }
882
+ // This one is where o starts; o is where this one starts; the same; o followed by `by` more; x at some position.
883
+ prefixOf(o) { return this.len.atMost(o.len).and(this.agree(o, this.len)); }
884
+ startsWith(o) { return o.prefixOf(this); }
885
+ equals(o) { return this.len.eq(o.len).and(this.agree(o, this.len)); }
886
+ extends(o, by) { return o.prefixOf(this).and(this.len.eq(o.len.plus(by))); }
887
+ has(x) { return everyKey(k => k.lt(this.len).implies(this.at(k).ne(x))).not(); }
888
+ }
889
+ // A table of sequences a request carries (each grant's path, say): `row(r)` is row r's sequence, `rows` how many. Rows
890
+ // are read by their number as a map's key, so a condition about every row reads a row only there (`everyBelow(T.rows,
891
+ // r => ...)`).
892
+ export class SeqTable {
893
+ name;
894
+ rows;
895
+ constructor(name, rows) {
896
+ this.name = name;
897
+ this.rows = rows;
898
+ }
899
+ row(r) { return new SeqRef(new MapRef(this.name, 0), new MapRef(`${this.name}_len`, 0).at(r), new MapRef(`${this.name}_base`, 0).at(r)); }
900
+ }
901
+ // f at every key below n: what bookkeeping a request carries meets, row by row (where each grant went, say).
902
+ export const everyBelow = (n, f) => everyKey(k => k.lt(n).implies(f(k)));
863
903
  const when_ = (c, then, otherwise) => new Term({ op: 'if', c: c.c, then: termOf(then), else: termOf(otherwise) }, c.ok === undefined ? undefined : valOf(c.ok ? then : otherwise));
864
904
  const isPartsWrite = (u) => typeof u === 'object' && u !== null && 'parts' in u;
865
905
  // The parts of a value written at a cell, by the names of the maps they go to.
@@ -995,7 +1035,7 @@ export class CarriedList {
995
1035
  .state({ i: 0, ...Object.fromEntries(folds.map(([f, d]) => [`acc$${f}`, typeof d.from === 'number' ? d.from : (p) => d.from(p)])) })
996
1036
  .step((env) => ({ i: env.i.plus(1), ...Object.fromEntries(folds.map(([f, d]) => [`acc$${f}`, d.step(env[`acc$${f}`], item(env.i), env)])) }));
997
1037
  const accs = (env) => Object.fromEntries(folds.map(([f]) => [f, env[`acc$${f}`]]));
998
- return proveRule(r, Object.entries(this.lemmas).map(([n, l]) => r.law(n, (env) => l(accs(env), env.i, env))));
1038
+ return proveRule(r, Object.entries(this.lemmas).map(([n, l]) => r.law(n, (env) => l(accs(env), env.i, env))), { search: false });
999
1039
  }
1000
1040
  // Every carried map it is kept in: the items' parts, then each fold's.
1001
1041
  get maps() { return [...this.kind.vars(this.name), ...Object.keys(this.folds).map(f => `${this.name}$${f}`)]; }
@@ -1360,6 +1400,10 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
1360
1400
  const stated = Object.entries(lemmas).map(([n, f]) => { const holds = f(named('x'), named('fx'), envAt(own)).c; only(`lemma "${n}"`, holds, ['x', 'fx', ...args], uses.map(([u]) => u)); return { name: n, holds }; });
1361
1401
  const pairs = Object.entries(between).map(([n, f]) => { const holds = f(named('x'), named('fx'), named('y'), named('fy'), envAt(own)).c; only(`law between "${n}"`, holds, ['x', 'fx', 'y', 'fy', ...args], uses.map(([u]) => u)); return { name: n, holds }; });
1362
1402
  const argKinds = Object.fromEntries(args.map(a => [a, nat]));
1403
+ // The recurrence as a cyclic proof's instance (core/cyclic.ts, GDP_CYCLIC=1): the facts of the functions it reads, from
1404
+ // a rule that reads them.
1405
+ const recurCase = (between) => ({ name, args, zero, next, lemmas: stated, between,
1406
+ facts: uses.length ? rule(`${name}'s functions`).fns(def.fns ?? {}).inputs({}).state({ z: 0 }).step((env) => ({ z: env.z })).machine.facts ?? [] : [] });
1363
1407
  const failed = (what, v) => { if (!(v.proven && v.checked))
1364
1408
  throw new Unproven(`${name}'s ${what}`, v); };
1365
1409
  // The lemmas: x counting up from 0 and fx its value, each lemma a law of that machine.
@@ -1374,7 +1418,11 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
1374
1418
  const r = r0.state({ x: 0, fx: ((p) => (zeroReads ? p.fx0 : zeroAt(args.map(a => p[a])))) })
1375
1419
  .step((env) => ({ x: env.x.plus(1), fx: def.next(env.x, env.fx, envAt(args.map(a => env[a]))) }));
1376
1420
  const laws = Object.entries(lemmas).map(([n, f]) => r.law(n, (env) => f(env.x, env.fx, envAt(args.map(a => env[a])))));
1377
- verdict = proveRule(r, laws);
1421
+ // (Its machine's proof timed alone: the recurrence is compared below as its own instance.)
1422
+ const t0 = performance.now();
1423
+ verdict = quietly(() => proveRule(r, laws, { search: false }));
1424
+ if (cyclicOn())
1425
+ crossCyclic('recurrence', name, verdict.proven && verdict.checked, () => { const { facts, specs } = recurNodes(recurCase([])); return proveCyclic(facts, specs); }, performance.now() - t0);
1378
1426
  failed('lemmas', verdict);
1379
1427
  proofs.lemmas = { machine: r.machine, laws };
1380
1428
  }
@@ -1411,7 +1459,9 @@ export const recur = (name, def, lemmas = {}, between = {}) => {
1411
1459
  .step((env) => ({ y: env.y.plus(1), fy: def.next(env.y, env.fy, envAt(at(env))) }));
1412
1460
  const laws = [...Object.entries(lemmas).map(([n, l]) => r.law(`${n} at y`, (env) => l(env.y, env.fy, envAt(at(env))))),
1413
1461
  ...Object.entries(between).map(([n, l]) => r.law(n, (env) => l(env.x, env.fx, env.y, env.fy, envAt(at(env)))))];
1414
- const v = proveRule(r, laws);
1462
+ const t0 = performance.now(), v = quietly(() => proveRule(r, laws, { search: false })), usualMs = performance.now() - t0;
1463
+ if (cyclicOn())
1464
+ crossCyclic('recurrence, between', name, v.proven && v.checked, () => { const { facts, specs } = recurNodes(recurCase(pairs)); return proveCyclic(facts, specs); }, usualMs);
1415
1465
  failed('laws between two points', v);
1416
1466
  proofs.between = { machine: r.machine, laws };
1417
1467
  verdict = { proven: true, checked: verdict.checked && v.checked };
@@ -1631,6 +1681,16 @@ export class Rule {
1631
1681
  facts(f) { return this.with({ facts: [...(this.spec.facts ?? []), f] }); }
1632
1682
  // Maps each request carries whole (a set of names, say: cells 0 or 1), every cell past them 0; read like any map.
1633
1683
  carries(...names) { return this.with({ carried: [...(this.spec.carried ?? []), ...names] }); }
1684
+ // Sequences each request carries (SeqRef): `.carriesSeq('Path')`, then `Path.at(i)`, `Path.len`, `A.prefixOf(B)`; the
1685
+ // request gives each as an array of naturals.
1686
+ carriesSeq(...names) {
1687
+ return this.with({ carried: [...(this.spec.carried ?? []), ...names], inputs: [...this.spec.inputs, ...names.map(n => [`${n}_len`, nat])], seqs: [...(this.spec.seqs ?? []), ...names] });
1688
+ }
1689
+ // Tables of sequences each request carries (SeqTable): `.carriesSeqs('Paths')`, then `Paths.row(r)`, `Paths.rows`; the
1690
+ // request gives each as an array of arrays of naturals.
1691
+ carriesSeqs(...names) {
1692
+ return this.with({ carried: [...(this.spec.carried ?? []), ...names.flatMap(n => [n, `${n}_base`, `${n}_len`])], inputs: [...this.spec.inputs, ...names.map(n => [`${n}_rows`, nat])], seqTables: [...(this.spec.seqTables ?? []), ...names] });
1693
+ }
1634
1694
  // A list of values of a kind each request carries, with folds over it and lemmas about the folds (CarriedList):
1635
1695
  // .carriesList('Lines', record({amount: nat}), {total: {from: 0, step: (acc, line) => acc.plus(line.amount)}})
1636
1696
  // Each fold's start and step, and each lemma, may read the params declared before it.
@@ -1846,6 +1906,15 @@ export class Rule {
1846
1906
  out[n] = k.value(n);
1847
1907
  for (const n of this.spec.carried ?? [])
1848
1908
  out[n] = new MapRef(n, 0);
1909
+ for (const n of this.spec.seqs ?? []) {
1910
+ delete out[`${n}_len`];
1911
+ out[n] = new SeqRef(new MapRef(n, 0), v(`${n}_len`));
1912
+ }
1913
+ for (const n of this.spec.seqTables ?? []) {
1914
+ for (const part of [`${n}_base`, `${n}_len`, `${n}_rows`])
1915
+ delete out[part];
1916
+ out[n] = new SeqTable(n, v(`${n}_rows`));
1917
+ }
1849
1918
  const ps = Object.fromEntries(this.spec.params.map(([n]) => [n, out[n]]));
1850
1919
  for (const [n, k, fs, ls] of this.spec.lists2 ?? []) {
1851
1920
  const l = new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0), ls, ps, this.spec.params);
@@ -2187,16 +2256,35 @@ export class Rule {
2187
2256
  // --- running it -----------------------------------------------------------------------------------------------------
2188
2257
  // Runtime values (numbers, booleans, choice names, null for a missing value) to the variables the machine reads.
2189
2258
  encodeParams(values) { return encode(this.spec.params, values); }
2190
- // A carried map is a Set of keys (each cell 1), an array of them, or a Map of key to natural.
2259
+ // A carried map is a Set of keys (each cell 1), an array of them, or a Map of key to natural; a sequence an array of
2260
+ // naturals, a table of them an array of such arrays.
2191
2261
  // A carried list is an array of its items' runtime values (its length and folds computed from them); `params`, what
2192
2262
  // folds that read the params are computed with.
2193
2263
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2194
2264
  encodeInput(values, params) {
2195
2265
  const at = params && this.paramValues(this.encodeParams(params));
2196
2266
  const lists = Object.fromEntries((this.spec.lists2 ?? []).map(([n, k, fs]) => [n, new CarriedList(n, k, v(`${n}_len`), fs, part => new MapRef(part, 0), {}, at).encode(values[n])]));
2197
- const out = encode(this.spec.inputs, { ...values, ...Object.fromEntries(Object.entries(lists).map(([n, l]) => [`${n}_len`, l.len])) });
2267
+ // A sequence: its values at their positions (no 0 cell), its length; a table: the rows one after another in one map,
2268
+ // each row's offset and length.
2269
+ const seqMaps = {}, seqLens = {};
2270
+ const cells = (xs, from = 0) => xs.flatMap((x, i) => (x ? [[from + i, x]] : []));
2271
+ for (const n of this.spec.seqs ?? []) {
2272
+ const xs = values[n] ?? [];
2273
+ seqMaps[n] = new Map(cells(xs));
2274
+ seqLens[`${n}_len`] = xs.length;
2275
+ }
2276
+ for (const n of this.spec.seqTables ?? []) {
2277
+ const rows = values[n] ?? [], base = [];
2278
+ rows.reduce((at, r) => (base.push(at), at + r.length), 0);
2279
+ seqMaps[n] = new Map(rows.flatMap((r, i) => cells(r, base[i])));
2280
+ seqMaps[`${n}_base`] = new Map(base.flatMap((b, i) => (b ? [[i, b]] : [])));
2281
+ seqMaps[`${n}_len`] = new Map(rows.flatMap((r, i) => (r.length ? [[i, r.length]] : [])));
2282
+ seqLens[`${n}_rows`] = rows.length;
2283
+ }
2284
+ const out = encode(this.spec.inputs, { ...values, ...seqLens, ...Object.fromEntries(Object.entries(lists).map(([n, l]) => [`${n}_len`, l.len])) });
2198
2285
  for (const l of Object.values(lists))
2199
2286
  Object.assign(out, l.maps);
2287
+ Object.assign(out, seqMaps);
2200
2288
  for (const n of this.spec.carried ?? []) {
2201
2289
  if (n in out)
2202
2290
  continue;
@@ -2432,14 +2520,16 @@ const withKinds = (r, laws) => [...r.kindLaws().filter(k => !laws.some(l => l.na
2432
2520
  // interpreter took. A rule that reads functions is skipped: the kernel knows them only by their facts (a recurrence's
2433
2521
  // induction machine reads the function it proves things about), so a run computing them proves nothing about the goal.
2434
2522
  const refuteBudget = { trials: 60, depth: 6, bound: 3, ms: 150 };
2435
- function refuteByRuns(r, laws, stepLaws = []) {
2523
+ // The search's own runs, before it looks for what the laws need: deeper and wider, from another seed.
2524
+ const deeperBudget = { trials: 600, depth: 24, bound: 8, ms: 1500, seed: 29 };
2525
+ function refuteByRuns(r, laws, stepLaws = [], budget = refuteBudget) {
2436
2526
  const m = r.machine;
2437
2527
  if (Object.keys(m.fns ?? {}).length || m.facts?.length)
2438
2528
  return undefined;
2439
- const run = interpret(m), rand = seeded(13), until = performance.now() + refuteBudget.ms, bound = refuteBudget.bound;
2529
+ const run = interpret(m), rand = seeded(budget.seed ?? 13), until = performance.now() + budget.ms, bound = budget.bound;
2440
2530
  const shown = (input) => Object.fromEntries(Object.entries(input).map(([n, x]) => [n, typeof x === 'number' ? x : Object.fromEntries(x)]));
2441
2531
  try {
2442
- for (let trial = 0; trial < refuteBudget.trials && performance.now() < until; trial++) {
2532
+ for (let trial = 0; trial < budget.trials && performance.now() < until; trial++) {
2443
2533
  const params = r.randomParams(rand, bound);
2444
2534
  if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
2445
2535
  continue;
@@ -2451,7 +2541,7 @@ function refuteByRuns(r, laws, stepLaws = []) {
2451
2541
  for (const { w, on } of watching)
2452
2542
  if (!('then' in w) && !on.start(st))
2453
2543
  return broken(w, '@start', st);
2454
- for (let d = 0, tries = 0; d < refuteBudget.depth && tries < 40; tries++) {
2544
+ for (let d = 0, tries = 0; d < budget.depth && tries < 40; tries++) {
2455
2545
  const input = r.randomInput(rand, bound, params);
2456
2546
  if (!allowed(m, run, params, st, input))
2457
2547
  continue;
@@ -2471,7 +2561,7 @@ function refuteByRuns(r, laws, stepLaws = []) {
2471
2561
  // variable of a kind is drawn as its kind draws (33 yes/nos drawn from 0..7 almost never make a lawful state).
2472
2562
  const steps1 = stepLaws.filter(sl => (sl.steps ?? 1) === 1), given = withKinds(r, laws);
2473
2563
  const wide = Object.keys(m.state).length + m.inputs.length >= 32, lawsHere = wide ? given : [], kinded = wide ? r.kindsOfState() : [];
2474
- for (let trial = 0; !m.history && (steps1.length || lawsHere.length) && trial < refuteBudget.trials * 4 && performance.now() < until; trial++) {
2564
+ for (let trial = 0; !m.history && (steps1.length || lawsHere.length) && trial < budget.trials * 4 && performance.now() < until; trial++) {
2475
2565
  const params = r.randomParams(rand, bound);
2476
2566
  if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
2477
2567
  continue;
@@ -2499,25 +2589,56 @@ function refuteByRuns(r, laws, stepLaws = []) {
2499
2589
  return undefined;
2500
2590
  }
2501
2591
  const refutedVerdict = (out) => ({ proven: false, checked: false, goal: out.goal, stuck: 'sat', witness: out.witness, ...(out.run ? { run: out.run } : {}) });
2502
- export function proveRule(r, laws) {
2592
+ export function proveRule(r, laws, opts = {}) {
2503
2593
  const refuted = refuteByRuns(r, laws);
2504
2594
  if (refuted)
2505
2595
  return refutedVerdict(refuted);
2506
2596
  const out = proveLaws(r.machine, withKinds(r, laws));
2507
- return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
2597
+ const v = { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
2598
+ return searched(r, laws, [], v, opts);
2508
2599
  }
2509
- export function proveSteps(r, laws, stepLaws) {
2600
+ export function proveSteps(r, laws, stepLaws, opts = {}) {
2510
2601
  const refuted = refuteByRuns(r, laws, stepLaws);
2511
2602
  if (refuted)
2512
2603
  return refutedVerdict(refuted);
2513
2604
  const out = proveStepLaws(r.machine, withKinds(r, laws), stepLaws);
2514
- return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
2605
+ const v = { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
2606
+ return searched(r, laws, stepLaws, v, opts);
2607
+ }
2608
+ // The search, where the prover alone didn't prove the laws and nothing refuted them: the lemmas first where the rule has
2609
+ // recurrences or carried lists (the rule made again with them), then invariants over what it then is. The verdict is the
2610
+ // prover's on the laws with what was found; when nothing is found, the prover's own (with why).
2611
+ function searched(r, laws, stepLaws, v, opts) {
2612
+ if ((v.proven && v.checked) || opts.search === false || v.run || v.stuck === 'sat' && !/@step|@\d+ steps/.test(v.goal ?? ''))
2613
+ return v;
2614
+ const deeper = refuteByRuns(r, laws, stepLaws, deeperBudget);
2615
+ if (deeper?.run)
2616
+ return refutedVerdict(deeper);
2617
+ const until = performance.now() + (typeof opts.search === 'object' && opts.search.ms !== undefined ? opts.search.ms : 10_000);
2618
+ let rule = r, lemmas;
2619
+ const { fns, lists } = r.parts;
2620
+ if (lists.length || fns.some(([, d]) => recurOf(d))) {
2621
+ const d = withDeadline(until, () => discover(r, laws, stepLaws, v));
2622
+ if (d.verdict.proven && d.verdict.checked)
2623
+ return { ...d.verdict, found: { lemmas: d.found } };
2624
+ if (Object.keys(d.found).length)
2625
+ (rule = d.rule, lemmas = d.found);
2626
+ }
2627
+ const ms = until - performance.now();
2628
+ if (ms < 500)
2629
+ return v;
2630
+ const inf = inferLaws(rule, laws, stepLaws, { ms, ...(rule === r ? { first: v } : {}) });
2631
+ if (!(inf.proven && inf.checked))
2632
+ return { ...v, why: [v.why, `the search found nothing that proves it in ${Math.round(inf.stats.ms)} ms`].filter(Boolean).join('; ') };
2633
+ const { found, stats: _stats, ...verdict } = inf;
2634
+ void _stats;
2635
+ return { ...verdict, found: { laws: found, ...(lemmas ? { lemmas } : {}) } };
2515
2636
  }
2516
2637
  export function proveLeadsTo(r, laws, l) {
2517
2638
  const out = proveLiveness(r.machine, withKinds(r, laws), l);
2518
2639
  return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
2519
2640
  }
2520
- export const proveDecision = (d) => (d.steps ? proveSteps(d.steps.rule, [], [d.steps.law]) : goalsVerdict([d.goal]));
2641
+ export const proveDecision = (d) => (d.steps ? proveSteps(d.steps.rule, [], [d.steps.law], { search: false }) : goalsVerdict([d.goal]));
2521
2642
  const seeded = (seed) => { let x = seed; return k => { x = (x * 1103515245 + 12345) % 2 ** 31; return Math.floor(x / 2 ** 31 * k); }; };
2522
2643
  // A decision's callbacks against its condition, on random params and requests: `given` and `then` each run with every
2523
2644
  // value known, against what the interpreter evaluates of the decision built from them.
@@ -2661,7 +2782,12 @@ export class FoldRule {
2661
2782
  }
2662
2783
  export const fold = (name) => new FoldRule({ name, params: [], inputs: [], pre: [], assume: [] });
2663
2784
  // A fold's laws proven over every tree (a rose fold's own laws, its children's lemmas, with them).
2664
- export const proveFold = (f, laws) => goalsVerdict(foldObligations(f.fold, [...(f.lemmas ?? []), ...laws]));
2785
+ export const proveFold = (f, laws) => {
2786
+ const all = [...(f.lemmas ?? []), ...laws], t0 = performance.now(), v = goalsVerdict(foldObligations(f.fold, all)), usualMs = performance.now() - t0;
2787
+ if (cyclicOn())
2788
+ crossCyclic('fold', f.fold.name, v.proven && v.checked, () => { const { facts, specs } = foldNodes(f.fold, all); return proveCyclic(facts, specs); }, usualMs);
2789
+ return v;
2790
+ };
2665
2791
  const asTerm = (t) => (t instanceof Term ? t : lit(t));
2666
2792
  // A fold over trees whose nodes have any number of children, as the kernel's binary fold (so the kernel and Lean's
2667
2793
  // fold_sound check it as they check any fold): a node's children become a chain of glue nodes, each folding one child
@@ -13,10 +13,11 @@
13
13
  // its witness is a counterexample to induction, so the candidate that rules it out (and the most of those seen) is
14
14
  // added; a chosen candidate that nothing supports, or that makes a proof slow, is dropped; the set proven as a whole
15
15
  // is the answer, after each chosen candidate the proof doesn't need is taken out again
16
+ import { quietly } from "./cyclic.js";
16
17
  import { any, atMost, evalC, get, interpret, n, not, plus, same, times, x } from "./machine.js";
17
18
  import { proveRule, proveSteps } from "./dsl.js";
18
19
  import { allowed, watch } from "./watch.js";
19
- import { proveLaw, proveLaws, proveStepLaws } from "../kernel/prove.js";
20
+ import { proveLaw, proveLaws, proveStepLaws, withDeadline } from "../kernel/prove.js";
20
21
  const KEY = 'key$found', AT = '@', K = '#';
21
22
  const seeded = (seed) => { let s = seed; return k => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
22
23
  const isRule = (t) => typeof t.kindsOfState === 'function';
@@ -106,13 +107,16 @@ const keyValue = (cell, w) => {
106
107
  return value;
107
108
  };
108
109
  export function inferLaws(target, laws, stepLaws = [], opts = {}) {
110
+ return quietly(() => withDeadline(performance.now() + (opts.ms ?? 20000), () => inferLawsSearch(target, laws, stepLaws, opts)));
111
+ }
112
+ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
109
113
  const t0 = performance.now(), until = t0 + (opts.ms ?? 20000), rule = isRule(target) ? target : undefined;
110
114
  const m = rule ? rule.machine : target;
111
115
  let proofs = 0;
112
116
  const prove = (ls) => {
113
117
  proofs++;
114
118
  if (rule)
115
- return stepLaws.length ? proveSteps(rule, ls, stepLaws) : proveRule(rule, ls);
119
+ return stepLaws.length ? proveSteps(rule, ls, stepLaws, { search: false }) : proveRule(rule, ls, { search: false });
116
120
  const out = stepLaws.length ? proveStepLaws(m, ls, stepLaws) : proveLaws(m, ls);
117
121
  return { proven: out.proven, checked: out.checked, ...(out.failed ? { goal: out.failed.goal, stuck: out.failed.stuck, ...(out.failed.witness ? { witness: out.failed.witness } : {}), ...(out.failed.why ? { why: out.failed.why } : {}) } : {}) };
118
122
  };
@@ -245,16 +249,18 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
245
249
  };
246
250
  const choose = (k, of) => { let r = 1; for (let i = 0; i < k; i++)
247
251
  r = r * (of - i) / (i + 1); return r; };
248
- const plain = atoms.flatMap((a, i) => (a.cell ? [] : [i])), P = plain.length;
252
+ // A choice's index orders its values arbitrarily: it is a guard, or alone (a bound on it reads as the values it
253
+ // allows), never a term among others.
254
+ const plain = atoms.flatMap((a, i) => (a.cell ? [] : [i])), terms = plain.filter(i => !modeOf(i)?.choice), P = terms.length;
249
255
  const scalarOk = (acc) => acc.some(([i]) => !atoms[i].param);
250
256
  pick(1, [1], plain, scalarOk, false);
251
- pick(2, choose(2, P) * 4 * mags.length ** 2 <= maxCandidates / 4 ? mags : [1], plain, scalarOk, false);
257
+ pick(2, choose(2, P) * 4 * mags.length ** 2 <= maxCandidates / 4 ? mags : [1], terms, scalarOk, false);
252
258
  if (choose(3, P) * 8 * mags.length ** 3 <= maxCandidates / 2)
253
- pick(3, mags, plain, scalarOk, false);
259
+ pick(3, mags, terms, scalarOk, false);
254
260
  else if (choose(3, P) * 8 <= maxCandidates / 2)
255
- pick(3, [1], plain, scalarOk, false);
261
+ pick(3, [1], terms, scalarOk, false);
256
262
  if (cellAtoms.length) {
257
- const all = atoms.map((_, i) => i), cellOk = (acc) => acc.some(([i]) => atoms[i].cell) && acc.filter(([i]) => !atoms[i].cell).length <= 1;
263
+ const all = atoms.flatMap((_, i) => (modeOf(i)?.choice ? [] : [i])), cellOk = (acc) => acc.some(([i]) => atoms[i].cell) && acc.filter(([i]) => !atoms[i].cell).length <= 1;
258
264
  pick(1, [1], cellAtoms, () => true, true);
259
265
  pick(2, [1], all, cellOk, true);
260
266
  if (choose(3, A) * 8 <= maxCandidates / 4)
@@ -329,7 +335,8 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
329
335
  return `${nameOf(f.unreachable.atom)} is never ${shownValue(f.unreachable.atom, f.unreachable.value)}`;
330
336
  const { l, r } = sides(f), within = allowedValues(f);
331
337
  const body = within ? `${nameOf(f.coef[0][0])} is ${within.map(v => shownValue(f.coef[0][0], v)).join(' or ')}` : `${l} <= ${r}`;
332
- const guarded = !f.guard ? body : 'keyNot' in f.guard ? `k = ${nameOf(f.guard.keyNot)} or ${body}` : `${nameOf(f.guard.atom)} is ${shownValue(f.guard.atom, f.guard.value)} → ${body}`;
338
+ const under = f.when ? (({ l: wl, r: wr }) => `${wl} <= ${wr} → `)(sides({ coef: f.when.coef, d: f.when.d, cells: false, size: 0 })) : '';
339
+ const guarded = !f.guard ? under + body : 'keyNot' in f.guard ? `k = ${nameOf(f.guard.keyNot)} or ${body}` : `${nameOf(f.guard.atom)} is ${shownValue(f.guard.atom, f.guard.value)} → ${under}${body}`;
333
340
  return f.cells ? `every k: ${guarded}` : guarded;
334
341
  };
335
342
  const sum = (es) => es.reduce((p, q) => plus(p, q), n(0));
@@ -347,12 +354,13 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
347
354
  if (f.unreachable)
348
355
  return notAt(f.unreachable.atom, f.unreachable.value);
349
356
  const lhs = sum([...partsOf(f, 1), ...(f.d < 0 ? [n(-f.d)] : [])]), rhs = sum([...partsOf(f, -1), ...(f.d > 0 ? [n(f.d)] : [])]);
350
- const body = atMost(lhs, rhs);
357
+ // Under a condition Σ ≤ d: its negation, −Σ ≤ −d − 1, or the body.
358
+ const body = f.when ? any(condOf({ coef: f.when.coef.map(([i, c]) => [i, -c]), d: -f.when.d - 1, cells: false, size: 0 }), atMost(lhs, rhs)) : atMost(lhs, rhs);
351
359
  return !f.guard ? body : 'keyNot' in f.guard ? any(same(x(KEY), x(atoms[f.guard.keyNot].parts[0][0])), body) : any(notAt(f.guard.atom, f.guard.value), body);
352
360
  };
353
361
  // The builder call, for a rule whose atoms are its own variables (naturals, or integers alone).
354
362
  const sourceOf = (f, text) => {
355
- if (!rule)
363
+ if (!rule || f.when)
356
364
  return undefined;
357
365
  const used = [...new Set([...f.coef.map(([i]) => i), ...(f.guard ? ['keyNot' in f.guard ? f.guard.keyNot : f.guard.atom] : []), ...(f.unreachable ? [f.unreachable.atom] : [])])];
358
366
  const ints = f.coef.filter(([i]) => atoms[i].int).length;
@@ -388,6 +396,13 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
388
396
  else if (valueOf(atoms[f.guard.atom], env) !== f.guard.value)
389
397
  return true;
390
398
  }
399
+ if (f.when) {
400
+ let w = 0;
401
+ for (const [i, c] of f.when.coef)
402
+ w += c * valueOf(atoms[i], env);
403
+ if (w > f.when.d)
404
+ return true;
405
+ }
391
406
  let s = 0;
392
407
  for (const [i, c] of f.coef)
393
408
  s += c * valueOf(atoms[i], env);
@@ -489,6 +504,97 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
489
504
  }
490
505
  return pickF;
491
506
  };
507
+ // A counterexample to induction no candidate rules out: forms false at it that every sample meets under a condition
508
+ // it meets too (one value, or a sum or difference of two, at most a threshold: 0, ±1, or its own value there), the
509
+ // choice (loc, say) at its value there as the guard. A condition no sample meets gives its negation instead. Only the
510
+ // eight values most unusual at it are read (furthest past the samples in its guard, then the rarest there).
511
+ const choiceMode = modes.find(mo => mo.choice && !atoms[mo.atom].cell);
512
+ const condAtoms = plain.filter(i => !atoms[i].param && i !== choiceMode?.atom);
513
+ const fromCti = (p) => {
514
+ const stop = Math.min(until, performance.now() + Math.max(300, (until - performance.now()) / 8));
515
+ const sc = p.scalar, pv = (coef) => { let v = 0; for (const [i, c] of coef)
516
+ v += c * valueOf(atoms[i], sc); return v; };
517
+ const gv = choiceMode ? valueOf(atoms[choiceMode.atom], sc) : undefined;
518
+ const inGuard = [];
519
+ for (let j = 0; j < NS; j++)
520
+ if (gv === undefined || valsS[choiceMode.atom][j] === gv)
521
+ inGuard.push(j);
522
+ const unusual = (i) => {
523
+ const v = valueOf(atoms[i], sc);
524
+ let lo = Infinity, hi = -Infinity, same = 0;
525
+ for (const j of inGuard) {
526
+ const w = valsS[i][j];
527
+ if (w < lo)
528
+ lo = w;
529
+ if (w > hi)
530
+ hi = w;
531
+ if (w === v)
532
+ same++;
533
+ }
534
+ return [Math.max(0, lo - v, v - hi), -same];
535
+ };
536
+ const focus = condAtoms.map(i => ({ i, u: unusual(i) })).sort((a, b) => b.u[0] - a.u[0] || b.u[1] - a.u[1]).slice(0, 8).map(x => x.i);
537
+ // Over the focus: one or two values with the magnitudes, three with ±1.
538
+ const bodies = [];
539
+ const signed = (ks) => ks.flatMap(k => [k, -k]);
540
+ for (const [a, i] of focus.entries()) {
541
+ bodies.push({ coef: [[i, 1]] }, { coef: [[i, -1]] });
542
+ for (const [b2, k] of focus.slice(a + 1).entries()) {
543
+ for (const c1 of signed(mags))
544
+ for (const c2 of signed(mags))
545
+ if (gcd(c1, c2) === 1)
546
+ bodies.push({ coef: [[i, c1], [k, c2]] });
547
+ for (const q of focus.slice(a + b2 + 2))
548
+ for (const c1 of [1, -1])
549
+ for (const c2 of [1, -1])
550
+ for (const c3 of [1, -1])
551
+ bodies.push({ coef: [[i, c1], [k, c2], [q, c3]] });
552
+ }
553
+ }
554
+ const conds = [];
555
+ for (const [a, i] of focus.entries()) {
556
+ conds.push([[i, 1]], [[i, -1]]);
557
+ for (const k of focus.slice(a + 1))
558
+ conds.push([[i, 1], [k, -1]], [[i, -1], [k, 1]], [[i, 1], [k, 1]]);
559
+ }
560
+ const out = [], guard = choiceMode ? { atom: choiceMode.atom, value: gv } : undefined;
561
+ for (const coef of conds) {
562
+ const here = pv(coef);
563
+ for (const d of [...new Set([here, 0, 1, -1])].filter(d => here <= d)) {
564
+ if (performance.now() > stop)
565
+ break;
566
+ const rows = [];
567
+ for (const j of inGuard)
568
+ if (at(coef, valsS, j) <= d)
569
+ rows.push(j);
570
+ const when = { coef, d }, base = sizeOf(coef) + 80;
571
+ if (!rows.length) {
572
+ out.push({ coef: coef.map(([i, c]) => [i, -c]), d: -d - 1, cells: false, ...(guard ? { guard } : {}), size: base });
573
+ continue;
574
+ }
575
+ const stride = Math.max(1, Math.ceil(rows.length / 600));
576
+ for (const [q, sh] of bodies.entries()) {
577
+ if (q % 256 === 0 && performance.now() > stop)
578
+ break;
579
+ const v = pv(sh.coef);
580
+ let most = -Infinity, ok = true;
581
+ for (let t = 0; t < rows.length; t += stride) {
582
+ const w = at(sh.coef, valsS, rows[t]);
583
+ if (w >= v) {
584
+ ok = false;
585
+ break;
586
+ }
587
+ if (w > most)
588
+ most = w;
589
+ }
590
+ if (ok && Number.isFinite(most))
591
+ out.push({ coef: sh.coef, d: most, cells: false, ...(guard ? { guard } : {}), when, size: base + sizeOf(sh.coef) });
592
+ }
593
+ }
594
+ }
595
+ // A stride may skip a sample that breaks a form: each is checked again at every sample in its guard.
596
+ return out.sort((x, y) => x.size - y.size).slice(0, 2000).filter(f => inGuard.every(j => holdsRow(f, rowsS[j]))).slice(0, 400);
597
+ };
492
598
  // The search proves one law at a time (proveLaw), each with every law assumed, and keeps what it proved while laws
493
599
  // are only added (more laws assumed keep a proof a proof); dropping one forgets it all. Once every law is proven so,
494
600
  // the set is proven as a whole, by the rule's own prover: that verdict is the answer.
@@ -545,7 +651,13 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
545
651
  const p = /@step|@\d+ steps/.test(last.goal) && last.witness ? witnessPoint(last.witness) : undefined;
546
652
  if (p)
547
653
  ctis.push(p);
548
- const next = p ? best(p) : undefined;
654
+ let next = p ? best(p) : undefined;
655
+ if (!next && p) {
656
+ const t = performance.now(), made = fromCti(p);
657
+ pool.push(...made);
658
+ next = best(p);
659
+ opts.progress?.('cti', { forms: made.length, ms: Math.round(performance.now() - t) });
660
+ }
549
661
  if (next) {
550
662
  chosen.push(next);
551
663
  continue;
@@ -0,0 +1,45 @@
1
+ import type { C, E } from '../core/machine.js';
2
+ import { type Goal, type Tree } from './kernel.js';
3
+ export type Fact = {
4
+ name: string;
5
+ keys: string[];
6
+ holds: C;
7
+ };
8
+ export type Use = {
9
+ node: number;
10
+ subst: Record<string, E>;
11
+ guard: C;
12
+ premises: Tree[];
13
+ descent: Tree;
14
+ };
15
+ export type CNode = {
16
+ name: string;
17
+ hyps: C[];
18
+ concl: C;
19
+ rank: E[];
20
+ insts: {
21
+ fact: number;
22
+ at: E[];
23
+ }[];
24
+ uses: Use[];
25
+ main: Tree;
26
+ };
27
+ export type Cyclic = {
28
+ facts: Fact[];
29
+ nodes: CNode[];
30
+ };
31
+ export declare const falseC: C;
32
+ export declare const lexLt: (as: E[], bs: E[]) => C;
33
+ export declare const instHyps: (facts: Fact[], insts: CNode["insts"]) => C[];
34
+ export declare const baseHyps: (facts: Fact[], n: Pick<CNode, "hyps" | "insts">) => C[];
35
+ export declare const useHyps: (nodes: Pick<CNode, "concl">[], uses: Pick<Use, "node" | "subst" | "guard">[]) => C[];
36
+ type Shape = Pick<CNode, 'name' | 'hyps' | 'concl' | 'rank' | 'insts'> & {
37
+ uses: Pick<Use, 'node' | 'subst' | 'guard'>[];
38
+ };
39
+ export declare const mainGoal: (facts: Fact[], nodes: Shape[], n: Shape) => Goal;
40
+ export declare const premiseGoal: (facts: Fact[], n: Shape, u: Shape["uses"][number], c: C, i: number) => Goal;
41
+ export declare const descentGoal: (facts: Fact[], n: Shape, u: Shape["uses"][number], m: Shape) => Goal;
42
+ export declare const factsAvoid: (facts: Fact[], subst: Record<string, E>) => boolean;
43
+ export declare function refuseCyclic(p: Cyclic): string | undefined;
44
+ export declare const checkCyclic: (p: Cyclic) => boolean;
45
+ export {};
@@ -0,0 +1,54 @@
1
+ import { check, substC, substE } from "./kernel.js";
2
+ const zero = { op: 'const', value: 0 };
3
+ export const falseC = { op: '<', a: zero, b: zero };
4
+ // as below bs in order: the first place they differ, as's is smaller (Lean's lexLtC, built the same way).
5
+ export const lexLt = (as, bs) => (as.length && bs.length
6
+ ? { op: 'or', xs: [{ op: '<', a: as[0], b: bs[0] }, { op: 'and', xs: [{ op: '=', a: as[0], b: bs[0] }, lexLt(as.slice(1), bs.slice(1))] }] }
7
+ : falseC);
8
+ const sub = (s) => ({ vars: s, maps: {}, flags: {} });
9
+ // A fact at the terms a node names: its keys paired with the terms (the shorter list decides, as Lean's zip does).
10
+ const keyed = (keys, at) => Object.fromEntries(keys.slice(0, at.length).map((key, i) => [key, at[i]]));
11
+ export const instHyps = (facts, insts) => insts.flatMap(p => { const f = facts[p.fact]; return f ? [substC(f.holds, sub(keyed(f.keys, p.at)))] : []; });
12
+ export const baseHyps = (facts, n) => [...n.hyps, ...instHyps(facts, n.insts)];
13
+ export const useHyps = (nodes, uses) => uses.flatMap(u => {
14
+ const m = nodes[u.node];
15
+ return m ? [{ op: 'or', xs: [{ op: 'not', x: u.guard }, substC(m.concl, sub(u.subst))] }] : [];
16
+ });
17
+ export const mainGoal = (facts, nodes, n) => ({ name: `${n.name}`, hyps: [...baseHyps(facts, n), ...useHyps(nodes, n.uses), { op: 'not', x: n.concl }] });
18
+ export const premiseGoal = (facts, n, u, c, i) => ({ name: `${n.name}: use of node ${u.node}, its hypothesis ${i}`, hyps: [...baseHyps(facts, n), u.guard, { op: 'not', x: substC(c, sub(u.subst)) }] });
19
+ export const descentGoal = (facts, n, u, m) => ({ name: `${n.name}: use of node ${u.node}, its rank below`, hyps: [...baseHyps(facts, n), u.guard, { op: 'not', x: lexLt(m.rank.map(e => substE(e, sub(u.subst))), n.rank) }] });
20
+ const occurs = (name, t) => JSON.stringify(t).includes(`"op":"var","name":${JSON.stringify(name)}`);
21
+ // The language Lean's twin has: no flag, no quantifier.
22
+ const plain = (t) => !/"op":"(?:flag|all)"/.test(JSON.stringify(t));
23
+ export const factsAvoid = (facts, subst) => Object.keys(subst).every(v => facts.every(f => f.keys.includes(v) || !occurs(v, f.holds)));
24
+ // Why a cyclic proof is refused, or undefined when it checks.
25
+ export function refuseCyclic(p) {
26
+ const { facts, nodes } = p;
27
+ if (!plain(facts) || !plain(nodes.map(n => [n.hyps, n.concl, n.rank, n.insts, n.uses.map(u => [u.subst, u.guard])])))
28
+ return 'a flag or a quantifier (no cyclic proof reads one)';
29
+ for (const f of facts)
30
+ if (new Set(f.keys).size !== f.keys.length)
31
+ return `fact ${f.name}: a key twice`;
32
+ for (const n of nodes) {
33
+ if (!check(mainGoal(facts, nodes, n), n.main))
34
+ return `${n.name}: its goal`;
35
+ for (const [j, u] of n.uses.entries()) {
36
+ const m = nodes[u.node];
37
+ if (!m)
38
+ return `${n.name}: use ${j} names no node`;
39
+ if (m.rank.length !== n.rank.length)
40
+ return `${n.name}: use ${j} compares ranks of different lengths`;
41
+ if (!factsAvoid(facts, u.subst))
42
+ return `${n.name}: use ${j} assigns a variable a fact reads`;
43
+ if (m.hyps.length !== u.premises.length)
44
+ return `${n.name}: use ${j} has ${u.premises.length} premise trees for ${m.hyps.length} hypotheses`;
45
+ for (const [i, c] of m.hyps.entries())
46
+ if (!check(premiseGoal(facts, n, u, c, i), u.premises[i]))
47
+ return `${n.name}: use ${j}, hypothesis ${i} of ${m.name}`;
48
+ if (!check(descentGoal(facts, n, u, m), u.descent))
49
+ return `${n.name}: use ${j} doesn't lower the rank`;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ export const checkCyclic = (p) => refuseCyclic(p) === undefined;
@@ -21,6 +21,11 @@ export declare const solveKinds: Record<string, {
21
21
  ms: number;
22
22
  rows: number;
23
23
  }>;
24
+ export declare let proveDump: ((goal: Goal) => void) | undefined;
25
+ export declare const setProveDump: (f: ((goal: Goal) => void) | undefined) => void;
26
+ export declare const cellProducts: {
27
+ on: boolean;
28
+ };
24
29
  export type Outcome = {
25
30
  tree: Tree;
26
31
  nodes: number;
@@ -30,10 +35,15 @@ export type Outcome = {
30
35
  path: Path;
31
36
  why?: string;
32
37
  };
38
+ export declare function withDeadline<T>(until: number, f: () => T): T;
33
39
  export declare function grow(goal: Goal, path?: Path, budget?: {
34
40
  nodes: number;
35
41
  until?: number;
42
+ units?: boolean;
36
43
  }): Outcome;
44
+ export declare const minimal: {
45
+ on: boolean;
46
+ };
37
47
  export type Report = {
38
48
  proven: boolean;
39
49
  nodes: number;