gdp-wired 0.20.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,6 +1,7 @@
1
1
  // Invariants nobody wrote: a law that holds but isn't inductive, proven with laws the search finds. Nothing here is
2
2
  // trusted; what it finds is proven by the kernel with the rule's own laws, as laws of the same rule.
3
- // - runs: random runs from the start (params and requests by their kinds, the assumption met) give reachable states
3
+ // - runs: random runs from the start (params and requests by their kinds, the assumption met) give reachable states,
4
+ // 3,000 kept as samples
4
5
  // - candidates: linear forms Σ c·v ≤ d over the state and the params (an integer as its value, its two naturals
5
6
  // together), up to three terms with small coefficients and the rule's own constants, each d the largest value the
6
7
  // runs reached (and the rule's constants just above it); the same forms where a yes/no, a choice or a small natural
@@ -14,11 +15,23 @@
14
15
  // added; a chosen candidate that nothing supports, or that makes a proof slow, is dropped; the set proven as a whole
15
16
  // is the answer, after each chosen candidate the proof doesn't need is taken out again
16
17
  import { quietly } from "./cyclic.js";
17
- import { any, atMost, evalC, get, interpret, n, not, plus, same, times, x } from "./machine.js";
18
+ import { any, atMost, divisorsIn, evalC, get, interpret, machineTerms, modName, n, not, plus, same, times, withArithmetic, x } from "./machine.js";
18
19
  import { proveRule, proveSteps } from "./dsl.js";
19
20
  import { allowed, watch } from "./watch.js";
20
21
  import { proveLaw, proveLaws, proveStepLaws, withDeadline } from "../kernel/prove.js";
21
- const KEY = 'key$found', AT = '@', K = '#';
22
+ import { work } from "./solver.js";
23
+ const KEY = 'key$found', AT = '@', K = '#', SOME = 'k$found';
24
+ // The counted clock's rates, units a millisecond: about the slowest tenth of what Modal's 1-vCPU containers do in each
25
+ // phase (`runs/modal/infer/after2`, GDP_INFER_TRACE=1: requests tried 85 a ms, rows 13,093, forms evaluated 11,240, the
26
+ // kernel's work 37; laws watched 198 by a fit), so a counted budget runs out no later than a timed one would there.
27
+ // The runs' requests: 16,000, 400 trials of a rule whose every request is allowed. Every search of the suite's and the
28
+ // frontier corpus's then keeps 3,000 samples, at least as many as 500 ms of wall time gave on 2 vCPU (`runs/modal/infer/
29
+ // before`'s cal2 jobs: 29 to 11,625 trials, 745 to 3,000 samples). Of 100 to 3,000 trials (`runs/modal/infer/grid`),
30
+ // 100 lost a corpus item (metered blocks) and 200 to 3,000 none: 400 is a step above the least that lost none.
31
+ export const inferRates = { tries: 100, rows: 10_000, evals: 10_000, watched: 100, work: 40 };
32
+ export const inferRuns = 16_000;
33
+ // GDP_INFER_TRACE=1: each phase's wall time beside what it counted, to stderr (the calibration's numbers).
34
+ const inferTrace = !!globalThis.process?.env?.GDP_INFER_TRACE;
22
35
  const seeded = (seed) => { let s = seed; return k => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
23
36
  const isRule = (t) => typeof t.kindsOfState === 'function';
24
37
  const gcd = (a, b) => (b === 0 ? Math.abs(a) : gcd(b, a % b));
@@ -110,17 +123,54 @@ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
110
123
  return quietly(() => withDeadline(performance.now() + (opts.ms ?? 20000), () => inferLawsSearch(target, laws, stepLaws, opts)));
111
124
  }
112
125
  function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
113
- const t0 = performance.now(), until = t0 + (opts.ms ?? 20000), rule = isRule(target) ? target : undefined;
126
+ const t0 = performance.now(), ms = opts.ms ?? 20000, wallUntil = t0 + ms, rule = isRule(target) ? target : undefined;
127
+ // The clock the budgets read (counted milliseconds since the start, or wall time where `timed`), and the stop.
128
+ const timed = !!opts.timed, w0 = work.n, used = { tries: 0, rows: 0, evals: 0, watched: 0 };
129
+ const now = () => (timed ? performance.now() - t0
130
+ : used.tries / inferRates.tries + used.rows / inferRates.rows + used.evals / inferRates.evals + used.watched / inferRates.watched + (work.n - w0) / inferRates.work);
131
+ let stopped = false;
132
+ const stop = () => stopped || (stopped = performance.now() > wallUntil);
133
+ const until = ms;
134
+ let tp = performance.now(), cp = 0, wp = w0;
135
+ const trace = (what, o = {}) => {
136
+ if (!inferTrace)
137
+ return;
138
+ const t = performance.now(), c = now();
139
+ globalThis.process.stderr.write(`[infer] ${what} ${JSON.stringify({ ms: Math.round(t - tp), counted: Math.round(c - cp), work: work.n - wp, ...used, ...o })}\n`);
140
+ tp = t;
141
+ cp = c;
142
+ wp = work.n;
143
+ };
144
+ if (rule) {
145
+ const ks = divisorsIn([...laws, ...stepLaws]);
146
+ if (ks.size)
147
+ rule.dividesBy(...ks);
148
+ }
114
149
  const m = rule ? rule.machine : target;
115
150
  let proofs = 0;
151
+ // The rule (or machine) the kernel proves the candidates with: once a remainder is among them, one that declares
152
+ // div$k and mod$k for its divisors (rule.withDivisors leaves the caller's rule as it was until a found law needs them).
153
+ // Only the laws proven read: a search none of whose chosen candidates is a remainder proves with the rule as it is.
154
+ const byDivisors = new Map();
155
+ const forLaws = (ls) => {
156
+ const ks = [...divisorsIn(ls.map(l => l.holds))].filter(k2 => !m.fns?.[modName(k2)]).sort((a, b) => a - b), key = ks.join(',');
157
+ let out = byDivisors.get(key);
158
+ if (!out) {
159
+ const r2 = ks.length ? rule?.withDivisors(...ks) : rule;
160
+ out = { r: r2, m: r2 ? r2.machine : withArithmetic(m, [], ks) };
161
+ byDivisors.set(key, out);
162
+ }
163
+ return out;
164
+ };
116
165
  const prove = (ls) => {
117
166
  proofs++;
118
- if (rule)
119
- return stepLaws.length ? proveSteps(rule, ls, stepLaws, { search: false }) : proveRule(rule, ls, { search: false });
120
- const out = stepLaws.length ? proveStepLaws(m, ls, stepLaws) : proveLaws(m, ls);
167
+ const { r: searchRule, m: mm } = forLaws(ls);
168
+ if (searchRule)
169
+ return stepLaws.length ? proveSteps(searchRule, ls, stepLaws, { search: false }) : proveRule(searchRule, ls, { search: false });
170
+ const out = stepLaws.length ? proveStepLaws(mm, ls, stepLaws) : proveLaws(mm, ls);
121
171
  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 } : {}) } : {}) };
122
172
  };
123
- const stats = (samples, candidates, alive) => ({ samples, candidates, alive, proofs, ms: Math.round(performance.now() - t0) });
173
+ const stats = (samples, candidates, alive) => ({ samples, candidates, alive, proofs, ms: Math.round(performance.now() - t0), stopped: stop() });
124
174
  const first = opts.first ?? prove(laws);
125
175
  if ((first.proven && first.checked) || first.run)
126
176
  return { ...first, found: [], stats: stats(0, 0, 0) };
@@ -138,9 +188,19 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
138
188
  for (const st of opts.seen ?? [])
139
189
  if (!m.params.length)
140
190
  keep({ params: {}, st });
191
+ trace('start');
141
192
  const mapsText = (st) => JSON.stringify(Object.values(st.maps).map(c => [...c]));
142
- const runsUntil = Math.min(until, performance.now() + (opts.runsMs ?? 500));
143
- for (let trial = 0, bound = 2; performance.now() < runsUntil; trial++, bound = trial % 20 === 0 ? (bound >= 1024 ? 2 : bound * 2) : bound) {
193
+ // Counted: `runs` requests tried at least, and on until the samples are full (a trial keeps its start at least, so as
194
+ // many trials as they hold fill them), the trials at most `runs` or that many. Timed: `runsMs` of wall time.
195
+ const runsTimed = timed || opts.runsMs !== undefined, runsUntil = Math.min(wallUntil, performance.now() + (opts.runsMs ?? 500)), least = opts.runs ?? inferRuns;
196
+ const moreRuns = (trial) => (runsTimed ? performance.now() < runsUntil : trial < Math.max(cap, least) && (used.tries < least || samples.length < cap) && !stop());
197
+ let trials = 0;
198
+ // (an assumption with "there exists", a value the request brings, may admit no request at a small bound, a multiple
199
+ // of 5 but 0, say: a trial whose steps moved nothing doubles the bound at once)
200
+ const skolems = m.inputs.some(i => i.startsWith('some$a'));
201
+ for (let trial = 0, bound = 2, stuck = false; moreRuns(trial); trial++, bound = trial % 20 === 0 || stuck ? (bound >= 1024 ? 2 : bound * 2) : bound) {
202
+ stuck = false;
203
+ trials++;
144
204
  const params = randomParams(bound);
145
205
  if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
146
206
  continue;
@@ -152,7 +212,9 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
152
212
  continue;
153
213
  }
154
214
  keep({ params, st });
215
+ let took = 0;
155
216
  for (let d = 0, tries = 0; d < 40 && tries < 200; tries++) {
217
+ used.tries++;
156
218
  const input = randomInput(params, bound);
157
219
  let next;
158
220
  try {
@@ -164,12 +226,16 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
164
226
  break;
165
227
  }
166
228
  d++;
167
- if (JSON.stringify(next.scalars) !== JSON.stringify(st.scalars) || mapsText(next) !== mapsText(st))
229
+ if (JSON.stringify(next.scalars) !== JSON.stringify(st.scalars) || mapsText(next) !== mapsText(st)) {
168
230
  keep({ params, st: next });
231
+ took++;
232
+ }
169
233
  st = next;
170
234
  }
235
+ stuck = skolems && took === 0;
171
236
  }
172
- opts.progress?.('runs', { samples: samples.length });
237
+ opts.progress?.('runs', { samples: samples.length, trials });
238
+ trace('runs', { samples: samples.length, trials });
173
239
  // --- atoms and rows --------------------------------------------------------------------------------------------------
174
240
  const scalars = Object.keys(m.state), stateSet = new Set(scalars), mapNames = Object.keys(m.maps ?? {}), mapSet = new Set(mapNames);
175
241
  const atoms = [];
@@ -276,11 +342,12 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
276
342
  // guard in one pass over (at most 1,500 of) the rows. Building candidates takes at most a quarter of the time.
277
343
  const guards = [...modes].sort((p, q) => Number(!!q.choice) - Number(!!p.choice) || Number(!!q.bool) - Number(!!p.bool)).slice(0, 8);
278
344
  const strideS = Math.max(1, Math.ceil(NS / 1500)), strideK = Math.max(1, Math.ceil(NK / 1500));
279
- const buildUntil = Math.min(until, performance.now() + (until - performance.now()) / 4);
345
+ const buildUntil = Math.min(until, now() + (until - now()) / 4);
280
346
  for (const [n_, { coef, cells }] of shapes.entries()) {
281
- if (forms.length > maxCandidates || (n_ % 256 === 0 && performance.now() > buildUntil))
347
+ if (forms.length > maxCandidates || (n_ % 256 === 0 && (now() > buildUntil || stop())))
282
348
  break;
283
349
  const vals = cells ? valsK : valsS, N = cells ? NK : NS, rows = cells ? rowsK : rowsS;
350
+ used.rows += N + (coef.length > 2 ? 0 : Math.ceil(N / (cells ? strideK : strideS)));
284
351
  let d = -Infinity;
285
352
  for (let j = 0; j < N; j++)
286
353
  d = Math.max(d, at(coef, vals, j));
@@ -313,7 +380,76 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
313
380
  for (const value of mode.values)
314
381
  if (NS && !valsS[mode.atom].includes(value))
315
382
  forms.push({ coef: [], d: 0, cells: false, unreachable: { atom: mode.atom, value }, size: 1 });
383
+ // Remainders: a value, or the sum or difference of two, at one remainder by k in every run (or in every run where a
384
+ // guard holds), k each divisor the rule and its laws read (modBy, divBy) and 2: x mod 2 = 0 for a counter that steps
385
+ // by 2, the parity of a sum that two counters keep. No form above states one (no linear bound does).
386
+ // The moduli: the divisors the rule and its laws read, then 2, 3, 4 and the rule's own constants up to 64 (six in all).
387
+ const divisors = [...new Set([...[...divisorsIn([...machineTerms(m), ...laws.map(l => l.holds)])].filter(k => k >= 2 && k <= 1_000_000).sort((p, q) => p - q), 2, 3, 4, ...consts.filter(k => k > 4 && k <= 64)])].slice(0, 6);
388
+ const modAtoms = terms.filter(i => !atoms[i].param && !modeOf(i)?.bool), modShapes = modAtoms.map(i => [[i, 1]]);
389
+ if (modAtoms.length * (modAtoms.length - 1) <= 2000)
390
+ for (const [a, i] of modAtoms.entries())
391
+ for (const j of modAtoms.slice(a + 1))
392
+ modShapes.push([[i, 1], [j, 1]], [[i, 1], [j, -1]]);
393
+ const residue = (v, k) => ((v % k) + k) % k;
394
+ // A shape whose runs reached two values or fewer is a bound's (above), not a remainder's.
395
+ const varied = (coef) => { const vs = new Set(); for (let j = 0; j < NS && vs.size < 3; j += strideS) {
396
+ used.rows++;
397
+ vs.add(at(coef, valsS, j));
398
+ } return vs.size >= 3; };
399
+ for (const coef of NS && opts.remainders !== false ? modShapes.filter(varied) : [])
400
+ for (const k of divisors) {
401
+ if (forms.length > maxCandidates || now() > buildUntil || stop())
402
+ break;
403
+ const all = new Set(), byGuard = guards.map(() => new Map());
404
+ used.rows += Math.ceil(NS / strideS);
405
+ for (let j = 0; j < NS; j += strideS) {
406
+ const r = residue(at(coef, valsS, j), k);
407
+ all.add(r);
408
+ guards.forEach((mode, g) => { const v = valsS[mode.atom][j], b = byGuard[g]; if (!b.has(v))
409
+ b.set(v, new Set()); b.get(v).add(r); });
410
+ }
411
+ const size = sizeOf(coef) + 30 + k;
412
+ if (all.size === 1) {
413
+ forms.push({ coef, d: 0, cells: false, mod: { k, r: [...all][0] }, size });
414
+ continue;
415
+ }
416
+ guards.forEach((mode, g) => { if (coef.some(([i]) => i === mode.atom))
417
+ return; for (const [value, rs] of byGuard[g])
418
+ if (rs.size === 1 && mode.values.includes(value))
419
+ forms.push({ coef, d: 0, cells: false, guard: { atom: mode.atom, value }, mod: { k, r: [...rs][0] }, size: size + 50 }); });
420
+ }
421
+ // Congruences, where remainders are off: a state value (or the sum or difference of two) whose every sample leaves
422
+ // one remainder by a constant of the rule's (2 first), with at least three values reached: ∃k. v = c·k + r. The
423
+ // remainder above says the same without a quantifier, so the kernel proves it as it stands (no witness to choose),
424
+ // under a guard where only some runs keep it, and Lean states it with %: that is the form tried by default.
425
+ if (opts.remainders === false && opts.existentials !== false && NS) {
426
+ const mods = [...new Set([2, 3, 4, ...consts.filter(k => k > 4 && k <= 64)])].slice(0, 6);
427
+ const congOf = (coef) => {
428
+ const vs = new Set();
429
+ used.rows += NS;
430
+ for (let j = 0; j < NS; j++)
431
+ vs.add(at(coef, valsS, j));
432
+ if (vs.size < 3)
433
+ return;
434
+ const nat = coef.every(([i, c]) => !atoms[i].int && c > 0), list = [...vs];
435
+ for (const c of mods) {
436
+ const r = ((list[0] % c) + c) % c;
437
+ if (list.every(x => ((x % c) + c) % c === r) && (!nat || list.every(x => x >= r)))
438
+ forms.push({ coef, d: 0, cells: false, cong: { mod: c, rest: r }, size: sizeOf(coef) + 40 + c });
439
+ }
440
+ };
441
+ const st = plain.filter(i => !atoms[i].param && !modeOf(i));
442
+ for (const i of st)
443
+ congOf([[i, 1]]);
444
+ if (st.length <= 24)
445
+ for (const [a, i] of st.entries())
446
+ for (const j of st.slice(a + 1)) {
447
+ congOf([[i, 1], [j, 1]]);
448
+ congOf([[i, 1], [j, -1]]);
449
+ }
450
+ }
316
451
  opts.progress?.('candidates', { atoms: A, shapes: shapes.length, forms: forms.length });
452
+ trace('candidates', { forms: forms.length });
317
453
  // --- how a form reads, and as a law ------------------------------------------------------------------------------------
318
454
  const nameOf = (i) => atoms[i].name;
319
455
  const shownValue = (i, value) => { const mo = modeOf(i); return mo?.choice ? (mo.choice[value] ?? String(value)) : mo?.bool ? (value === 1 ? 'yes' : 'no') : String(value); };
@@ -333,13 +469,18 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
333
469
  const textOf = (f) => {
334
470
  if (f.unreachable)
335
471
  return `${nameOf(f.unreachable.atom)} is never ${shownValue(f.unreachable.atom, f.unreachable.value)}`;
472
+ if (f.cong)
473
+ return `∃k. ${f.coef.map(([i, c], j) => `${j ? (c > 0 ? ' + ' : ' − ') : c < 0 ? '−' : ''}${nameOf(i)}`).join('')} = ${f.cong.mod}·k${f.cong.rest ? ` + ${f.cong.rest}` : ''}`;
336
474
  const { l, r } = sides(f), within = allowedValues(f);
337
- const body = within ? `${nameOf(f.coef[0][0])} is ${within.map(v => shownValue(f.coef[0][0], v)).join(' or ')}` : `${l} <= ${r}`;
475
+ const body = f.mod ? `${f.coef.map(([i, c], j) => `${c < 0 ? (j ? ' − ' : '−') : j ? ' + ' : ''}${nameOf(i)}`).join('')} mod ${f.mod.k} = ${f.mod.r}` : within ? `${nameOf(f.coef[0][0])} is ${within.map(v => shownValue(f.coef[0][0], v)).join(' or ')}` : `${l} <= ${r}`;
338
476
  const under = f.when ? (({ l: wl, r: wr }) => `${wl} <= ${wr} → `)(sides({ coef: f.when.coef, d: f.when.d, cells: false, size: 0 })) : '';
339
477
  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}`;
340
478
  return f.cells ? `every k: ${guarded}` : guarded;
341
479
  };
342
480
  const sum = (es) => es.reduce((p, q) => plus(p, q), n(0));
481
+ // Each congruence its own witness's name (each may be a ghost of the machine).
482
+ const someNames = new Map(), someName = (f) => { let k = someNames.get(f); if (!k)
483
+ someNames.set(f, (k = `${SOME}${someNames.size}`)); return k; };
343
484
  const part = (a, p) => (a.cell ? get(p, x(KEY)) : x(p));
344
485
  const partsOf = (f, sign) => f.coef.flatMap(([i, c]) => atoms[i].parts.filter(([, s]) => s * c * sign > 0).map(([p]) => { const e = part(atoms[i], p); return Math.abs(c) === 1 ? e : times(Math.abs(c), e); }));
345
486
  // Σ c·v ≤ d over naturals: the positive parts on the left, the negative ones and d on the right (an integer's
@@ -353,6 +494,19 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
353
494
  };
354
495
  if (f.unreachable)
355
496
  return notAt(f.unreachable.atom, f.unreachable.value);
497
+ if (f.mod) {
498
+ // Σ c·v = P − N over naturals (P the positive parts, N the negative ones), whose remainder by k is that of the
499
+ // natural P + (k − 1)·N.
500
+ const { k: dk, r } = f.mod, neg = partsOf(f, -1), e = get(modName(dk), sum([...partsOf(f, 1), ...neg.map(t => times(dk - 1, t))]));
501
+ const body = same(e, n(r));
502
+ 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);
503
+ }
504
+ // Σ = c·k + r, k = SOME (an integer's two parts where Σ reads an integer or a difference): the negative parts and
505
+ // c·k's negative part on the other side.
506
+ if (f.cong) {
507
+ const k = someName(f), intK = f.coef.some(([i, c]) => atoms[i].int || c < 0), ck = times(f.cong.mod, x(k)), ckNeg = times(f.cong.mod, x(`${k}$neg`));
508
+ return same(sum([...partsOf(f, 1), ...(intK ? [ckNeg] : [])]), sum([...partsOf(f, -1), ck, ...(f.cong.rest ? [n(f.cong.rest)] : [])]));
509
+ }
356
510
  const lhs = sum([...partsOf(f, 1), ...(f.d < 0 ? [n(-f.d)] : [])]), rhs = sum([...partsOf(f, -1), ...(f.d > 0 ? [n(f.d)] : [])]);
357
511
  // Under a condition Σ ≤ d: its negation, −Σ ≤ −d − 1, or the body.
358
512
  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);
@@ -360,7 +514,7 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
360
514
  };
361
515
  // The builder call, for a rule whose atoms are its own variables (naturals, or integers alone).
362
516
  const sourceOf = (f, text) => {
363
- if (!rule || f.when)
517
+ if (!rule || f.when || f.mod)
364
518
  return undefined;
365
519
  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] : [])])];
366
520
  const ints = f.coef.filter(([i]) => atoms[i].int).length;
@@ -373,6 +527,12 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
373
527
  const wrap = (body) => (f.cells ? `forall(k => ${body})` : body);
374
528
  if (f.unreachable)
375
529
  return `r.law('${text}', ${args} => ${cond(f.unreachable.atom, f.unreachable.value)}.not())`;
530
+ if (f.cong) {
531
+ if (f.coef.length !== 1 || f.coef[0][1] !== 1)
532
+ return undefined;
533
+ const intK = atoms[f.coef[0][0]].int;
534
+ return `r.law('${text}', ${args} => exists(${intK ? 'int, ' : ''}k => ${val(f.coef[0][0])}.eq(k.times(${f.cong.mod})${f.cong.rest ? `.plus(${f.cong.rest})` : ''})))`;
535
+ }
376
536
  const term = (ts, k) => {
377
537
  const parts = ts.map(([i, c]) => (Math.abs(c) === 1 ? val(i) : `${val(i)}.times(${Math.abs(c)})`));
378
538
  if (!parts.length)
@@ -385,28 +545,49 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
385
545
  return `r.law('${text}', ${args} => ${wrap(guarded)})`;
386
546
  };
387
547
  // --- evaluating a form ---------------------------------------------------------------------------------------------
548
+ // (each row's atom values read once, however many forms are evaluated there: rows are never changed once made)
549
+ const valuesAt = new WeakMap();
550
+ const valuesOf = (env) => {
551
+ let vs = valuesAt.get(env);
552
+ if (!vs) {
553
+ vs = new Float64Array(A);
554
+ for (let i = 0; i < A; i++)
555
+ vs[i] = valueOf(atoms[i], env);
556
+ valuesAt.set(env, vs);
557
+ }
558
+ return vs;
559
+ };
388
560
  const holdsRow = (f, env) => {
561
+ used.evals++;
562
+ const vs = valuesOf(env);
389
563
  if (f.unreachable)
390
- return valueOf(atoms[f.unreachable.atom], env) !== f.unreachable.value;
564
+ return vs[f.unreachable.atom] !== f.unreachable.value;
565
+ if (f.cong) {
566
+ let v = 0;
567
+ for (const [i, c] of f.coef)
568
+ v += c * vs[i];
569
+ const nat = f.coef.every(([i, c]) => !atoms[i].int && c > 0);
570
+ return ((v - f.cong.rest) % f.cong.mod + f.cong.mod) % f.cong.mod === 0 && (!nat || v >= f.cong.rest);
571
+ }
391
572
  if (f.guard) {
392
573
  if ('keyNot' in f.guard) {
393
- if (env[K] === valueOf(atoms[f.guard.keyNot], env))
574
+ if (env[K] === vs[f.guard.keyNot])
394
575
  return true;
395
576
  }
396
- else if (valueOf(atoms[f.guard.atom], env) !== f.guard.value)
577
+ else if (vs[f.guard.atom] !== f.guard.value)
397
578
  return true;
398
579
  }
399
580
  if (f.when) {
400
581
  let w = 0;
401
582
  for (const [i, c] of f.when.coef)
402
- w += c * valueOf(atoms[i], env);
583
+ w += c * vs[i];
403
584
  if (w > f.when.d)
404
585
  return true;
405
586
  }
406
587
  let s = 0;
407
588
  for (const [i, c] of f.coef)
408
- s += c * valueOf(atoms[i], env);
409
- return s <= f.d;
589
+ s += c * vs[i];
590
+ return f.mod ? residue(s, f.mod.k) === f.mod.r : s <= f.d;
410
591
  };
411
592
  const pointOf = (params, st) => ({ scalar: { ...params, ...st.scalars }, cells: cellRows(params, st) });
412
593
  const holdsAt = (f, p) => (f.cells ? p.cells.every(r => holdsRow(f, r)) : holdsRow(f, p.scalar));
@@ -430,16 +611,16 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
430
611
  // --- Houdini on the runs' neighbors ----------------------------------------------------------------------------------
431
612
  let alive = forms.slice().sort((p, q) => p.size - q.size).slice(0, maxCandidates);
432
613
  const ctis = [];
433
- const lawful = (params, st) => { try {
614
+ const lawful = (params, st) => { used.watched += given.length; try {
434
615
  return given.every(l => watch(m, run, l, params, 4).start(st));
435
616
  }
436
617
  catch {
437
618
  return false;
438
619
  } };
439
- const houdiniUntil = Math.min(until, performance.now() + Math.max(500, (until - performance.now()) / 4));
440
- for (let quiet = 0; samples.length && performance.now() < houdiniUntil && quiet < 3;) {
620
+ const houdiniUntil = Math.min(until, now() + Math.max(500, (until - now()) / 4));
621
+ for (let quiet = 0; samples.length && now() < houdiniUntil && !stop() && quiet < 3;) {
441
622
  const killed = new Set();
442
- for (let tries = 0; tries < 400 && performance.now() < houdiniUntil; tries++) {
623
+ for (let tries = 0; tries < 400 && now() < houdiniUntil && !stop(); tries++) {
443
624
  const base = samples[rand(samples.length)], scal = { ...base.st.scalars }, maps = { ...base.st.maps };
444
625
  for (let k = 0, moves = 1 + rand(2); k < moves; k++) {
445
626
  if (mapNames.length && rand(2) === 0) {
@@ -480,23 +661,27 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
480
661
  alive = alive.filter(f => !killed.has(f));
481
662
  }
482
663
  opts.progress?.('houdini', { alive: alive.length, ctis: ctis.length });
664
+ trace('houdini', { alive: alive.length });
483
665
  // --- the kernel ----------------------------------------------------------------------------------------------------
484
666
  const lawOf = new Map();
667
+ const someOf = (f) => (!f.cong ? [] : f.coef.some(([i, c]) => atoms[i].int || c < 0) ? [someName(f), `${someName(f)}$neg`] : [someName(f)]);
485
668
  const lawFor = (f) => { let l = lawOf.get(f); if (!l)
486
- lawOf.set(f, (l = { name: textOf(f), holds: condOf(f), ...(f.cells ? { forall: KEY } : {}) })); return l; };
669
+ lawOf.set(f, (l = { name: textOf(f), holds: condOf(f), ...(f.cells ? { forall: KEY } : {}), ...(f.cong ? { some: someOf(f) } : {}) })); return l; };
487
670
  const names = new Set(given.map(l => l.name));
488
671
  alive = alive.filter(f => !names.has(textOf(f)));
489
672
  let pool = alive.slice(), chosen = [];
490
673
  const goalName = (g) => g.replace(/ @[^@]*$/, '');
491
674
  let last = first;
492
- const slow = Math.max(3000, (opts.ms ?? 20000) / 10);
675
+ const slow = Math.max(3000, ms / 10);
493
676
  // A point a form is false at; a cell form is judged only at rows that read every map it reads.
494
677
  const reads = (f, r) => f.coef.every(([i]) => !atoms[i].cell || atoms[i].parts.every(([q]) => AT + q in r));
495
678
  const falseAt = (f, p) => (f.cells ? p.cells.some(r => reads(f, r) && !holdsRow(f, r)) : !holdsRow(f, p.scalar));
496
- const best = (p) => {
679
+ // (a remainder or a congruence only where no bound of the candidates rules the point out, before the counterexample's own forms:
680
+ // false at half the states a run doesn't reach, it would otherwise be chosen first everywhere)
681
+ const best = (p, congs = false) => {
497
682
  let pickF, score = -Infinity;
498
683
  for (const f of pool) {
499
- if (chosen.includes(f) || !falseAt(f, p))
684
+ if (chosen.includes(f) || !!(f.cong || f.mod) !== congs || !falseAt(f, p))
500
685
  continue;
501
686
  const s = ctis.reduce((k, c) => k + (falseAt(f, c) ? 1 : 0), 0) * 1000 - f.size;
502
687
  if (s > score)
@@ -511,7 +696,7 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
511
696
  const choiceMode = modes.find(mo => mo.choice && !atoms[mo.atom].cell);
512
697
  const condAtoms = plain.filter(i => !atoms[i].param && i !== choiceMode?.atom);
513
698
  const fromCti = (p) => {
514
- const stop = Math.min(until, performance.now() + Math.max(300, (until - performance.now()) / 8));
699
+ const end = Math.min(until, now() + Math.max(300, (until - now()) / 8));
515
700
  const sc = p.scalar, pv = (coef) => { let v = 0; for (const [i, c] of coef)
516
701
  v += c * valueOf(atoms[i], sc); return v; };
517
702
  const gv = choiceMode ? valueOf(atoms[choiceMode.atom], sc) : undefined;
@@ -561,9 +746,10 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
561
746
  for (const coef of conds) {
562
747
  const here = pv(coef);
563
748
  for (const d of [...new Set([here, 0, 1, -1])].filter(d => here <= d)) {
564
- if (performance.now() > stop)
749
+ if (now() > end || stop())
565
750
  break;
566
751
  const rows = [];
752
+ used.rows += inGuard.length;
567
753
  for (const j of inGuard)
568
754
  if (at(coef, valsS, j) <= d)
569
755
  rows.push(j);
@@ -574,9 +760,10 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
574
760
  }
575
761
  const stride = Math.max(1, Math.ceil(rows.length / 600));
576
762
  for (const [q, sh] of bodies.entries()) {
577
- if (q % 256 === 0 && performance.now() > stop)
763
+ if (q % 256 === 0 && (now() > end || stop()))
578
764
  break;
579
765
  const v = pv(sh.coef);
766
+ used.rows += Math.ceil(rows.length / stride);
580
767
  let most = -Infinity, ok = true;
581
768
  for (let t = 0; t < rows.length; t += stride) {
582
769
  const w = at(sh.coef, valsS, rows[t]);
@@ -604,11 +791,18 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
604
791
  done.add(b); };
605
792
  const drop = (f) => { chosen = chosen.filter(g => g !== f); pool = pool.filter(g => g !== f); forget(); };
606
793
  const search = (ls) => {
794
+ // (laws with "there exists" are proven as a set, their witnesses chosen together: core/exists.ts)
795
+ if (ls.some(l => l.some?.length)) {
796
+ const whole = prove([...laws, ...chosen.map(lawFor)]);
797
+ if (!(whole.proven && whole.checked))
798
+ forget();
799
+ return whole;
800
+ }
607
801
  for (let i = 0; i < ls.length; i++) {
608
802
  if (done.has(ls[i].name))
609
803
  continue;
610
804
  proofs++;
611
- const r = proveLaw(m, ls, i);
805
+ const r = proveLaw(forLaws(ls).m, ls, i);
612
806
  if (!r.proven)
613
807
  return { proven: false, checked: false, goal: r.goal, stuck: r.stuck, ...(r.witness ? { witness: r.witness } : {}) };
614
808
  done.add(ls[i].name);
@@ -617,7 +811,7 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
617
811
  }
618
812
  if (stepLaws.length) {
619
813
  proofs++;
620
- const r = proveStepLaws(m, ls, stepLaws);
814
+ const r = proveStepLaws(forLaws(ls).m, ls, stepLaws);
621
815
  if (!r.proven)
622
816
  return { proven: false, checked: false, ...(r.failed ? { goal: r.failed.goal, stuck: r.failed.stuck, ...(r.failed.witness ? { witness: r.failed.witness } : {}) } : {}) };
623
817
  }
@@ -626,11 +820,11 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
626
820
  forget();
627
821
  return whole;
628
822
  };
629
- for (let iter = 0; iter < 300 && performance.now() < until; iter++) {
630
- const began = performance.now();
823
+ for (let iter = 0; iter < 300 && now() < until && !stop(); iter++) {
824
+ const began = now(), wall = performance.now();
631
825
  last = search([...given, ...chosen.map(lawFor)]);
632
- const took = performance.now() - began;
633
- opts.progress?.('proof', { chosen: chosen.length, goal: last.goal ?? '', stuck: last.stuck ?? '', ms: Math.round(took) });
826
+ const took = now() - began;
827
+ opts.progress?.('proof', { chosen: chosen.length, goal: last.goal ?? '', stuck: last.stuck ?? '', ms: Math.round(performance.now() - wall), counted: Math.round(took) });
634
828
  if (last.proven && last.checked)
635
829
  break;
636
830
  if (!last.goal)
@@ -652,6 +846,8 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
652
846
  if (p)
653
847
  ctis.push(p);
654
848
  let next = p ? best(p) : undefined;
849
+ if (!next && p)
850
+ next = best(p, true);
655
851
  if (!next && p) {
656
852
  const t = performance.now(), made = fromCti(p);
657
853
  pool.push(...made);
@@ -671,15 +867,36 @@ function inferLawsSearch(target, laws, stepLaws = [], opts = {}) {
671
867
  break;
672
868
  drop(stuckOn);
673
869
  }
674
- if (!(last.proven && last.checked))
870
+ // Unproven: each congruence the runs left alive, with the laws alone (the counterexamples' choice may have gone to the
871
+ // bounds instead), at most four.
872
+ if (!(last.proven && last.checked)) {
873
+ for (const f of alive.filter(g => g.cong).slice(0, 4)) {
874
+ if (now() > until || stop())
875
+ break;
876
+ const v = prove([...laws, lawFor(f)]);
877
+ if (v.proven && v.checked) {
878
+ chosen = [f];
879
+ last = v;
880
+ break;
881
+ }
882
+ }
883
+ }
884
+ trace('kernel', { chosen: chosen.length, proven: !!(last.proven && last.checked) });
885
+ if (!(last.proven && last.checked)) {
886
+ trace('end', { proven: false, stopped: stop() });
675
887
  return { ...last, found: [], stats: stats(samples.length, forms.length, alive.length) };
888
+ }
676
889
  // Take out what the proof doesn't need, the last added first.
677
890
  for (const f of [...chosen].reverse()) {
678
- if (performance.now() > until)
891
+ if (now() > until || stop())
679
892
  break;
680
893
  const without = chosen.filter(g => g !== f), v = prove([...laws, ...without.map(lawFor)]);
681
894
  if (v.proven && v.checked)
682
895
  (chosen = without, last = v);
683
896
  }
897
+ const kept = chosen.flatMap(f => (f.mod ? [f.mod.k] : []));
898
+ if (kept.length)
899
+ rule?.dividesBy(...kept);
900
+ trace('end', { proven: true, stopped: stop(), found: chosen.map(f => textOf(f)).join('; ') });
684
901
  return { ...last, found: chosen.map(f => { const law = lawFor(f), text = law.name, source = sourceOf(f, text); return { law, text, ...(source ? { source } : {}) }; }), stats: stats(samples.length, forms.length, alive.length) };
685
902
  }
@@ -134,6 +134,22 @@ export type Law = {
134
134
  forall?: string;
135
135
  keys?: string[];
136
136
  stated?: C;
137
+ some?: string[];
138
+ witness?: SomeWitness;
139
+ ex?: SomeProof;
140
+ };
141
+ export type SomeWitness = {
142
+ terms?: Record<string, E>;
143
+ ghost?: Record<string, {
144
+ start: E;
145
+ step: E;
146
+ }>;
147
+ };
148
+ export type SomeProof = {
149
+ vars: string[];
150
+ body: C;
151
+ sub?: Record<string, E>;
152
+ ghost?: true;
137
153
  };
138
154
  export { unreachable } from '../kernel/kernel.js';
139
155
  export declare const x: (name: string) => E;
@@ -153,6 +169,17 @@ export declare const all: (...xs: C[]) => C;
153
169
  export declare const any: (...xs: C[]) => C;
154
170
  export declare const not: (c: C) => C;
155
171
  export declare const bit: (name: string) => C;
172
+ export declare const divName: (k: number) => string;
173
+ export declare const modName: (k: number) => string;
174
+ export declare const isArithmetic: (name: string) => boolean;
175
+ export declare const divisor: (k: number, what: string) => number;
176
+ export declare function divisorsIn(ts: readonly (E | C)[], out?: Set<number>): Set<number>;
177
+ export declare function arithmetic(ks: Iterable<number>): {
178
+ fns: Record<string, Fn>;
179
+ facts: Law[];
180
+ };
181
+ export declare function withArithmetic(m: Machine, more?: readonly (E | C)[], ks?: Iterable<number>): Machine;
182
+ export declare const machineTerms: (m: Machine) => (E | C)[];
156
183
  export type State = {
157
184
  scalars: Record<string, number>;
158
185
  maps: Record<string, Map<number, number>>;
@@ -33,6 +33,51 @@ export const all = (...xs) => ({ op: 'and', xs });
33
33
  export const any = (...xs) => ({ op: 'or', xs });
34
34
  export const not = (c) => ({ op: 'not', x: c });
35
35
  export const bit = (name) => any(same(x(name), n(0)), same(x(name), n(1)));
36
+ // --- division and remainder by a constant ------------------------------------------------------------------------
37
+ // x div k and x mod k, for a constant k >= 1, are reads of two functions of the rule, div$k and mod$k, at x. The kernel
38
+ // knows them by two facts: x = k·div$k(x) + mod$k(x), and mod$k(x) < k (both naturals, as every value is), which fix
39
+ // both values (Euclid's division), so a goal about them is Presburger arithmetic the kernel already decides: nothing
40
+ // new is trusted. The runtimes compute them as Math.floor(x / k) and x % k; the Lean export defines them as Lean's
41
+ // x / k and x % k and proves the two facts of those by omega.
42
+ export const divName = (k) => `div$${k}`;
43
+ export const modName = (k) => `mod$${k}`;
44
+ const arithRe = /^(div|mod)\$(\d+)$/;
45
+ export const isArithmetic = (name) => arithRe.test(name);
46
+ export const divisor = (k, what) => {
47
+ if (!Number.isSafeInteger(k) || k < 1)
48
+ throw new RangeError(`${what} divides by a whole number at least 1, not ${k}`);
49
+ return k;
50
+ };
51
+ // The constants some terms divide by.
52
+ export function divisorsIn(ts, out = new Set()) {
53
+ for (const t of ts)
54
+ for (const m of JSON.stringify(t).matchAll(/"op":"get","map":"(?:div|mod)\$(\d+)"/g))
55
+ out.add(Number(m[1]));
56
+ return out;
57
+ }
58
+ // The two functions and their facts for each divisor: what a machine that divides by k declares.
59
+ export function arithmetic(ks) {
60
+ const fns = {}, facts = [];
61
+ for (const k of [...new Set(ks)].sort((a, b) => a - b)) {
62
+ const d = divName(k), r = modName(k), key = `x$${r}`, at = (map) => get(map, x(key));
63
+ fns[d] = { js: `x => Math.floor(x / ${k})`, lean: `fun x => x / ${k}` };
64
+ fns[r] = { js: `x => x % ${k}`, lean: `fun x => x % ${k}` };
65
+ facts.push({ name: `${r}: x = ${k}·(x div ${k}) + x mod ${k}`, forall: key, holds: same(x(key), plus(times(k, at(d)), at(r))) }, { name: `${r}: x mod ${k} < ${k}`, forall: key, holds: less(at(r), n(k)) });
66
+ }
67
+ return { fns, facts };
68
+ }
69
+ // A machine with the functions and facts of every divisor its terms (or the laws given) read, those it lacks added.
70
+ export function withArithmetic(m, more = [], ks = []) {
71
+ const all = divisorsIn(more, divisorsIn(machineTerms(m), new Set(ks)));
72
+ const { fns, facts } = arithmetic([...all].filter(k => !m.fns?.[modName(k)]));
73
+ if (!Object.keys(fns).length)
74
+ return m;
75
+ return { ...m, fns: { ...m.fns, ...fns }, facts: [...(m.facts ?? []), ...facts] };
76
+ }
77
+ // Every term and condition a machine holds.
78
+ export const machineTerms = (m) => [...Object.values(m.state), ...Object.values(m.step.flags ?? {}), ...Object.values(m.step.state ?? {}),
79
+ ...Object.values(m.step.maps ?? {}).flatMap(w => ('key' in w ? [w.key, w.value] : [w.value])), ...(m.pre ? [m.pre] : []), ...(m.assume ? [m.assume] : []),
80
+ ...(m.facts ?? []).map(f => f.holds), ...(m.history ? [m.history.keep, ...Object.values(m.history.sums), ...Object.values(m.history.queries), ...(m.history.relations ?? []).flatMap(r => [r.when, r.holds])] : [])];
36
81
  // A condition at every key, or a value at every key. Where the bound key is read only as a map's key, each key a map
37
82
  // read holds a cell at, and one key past them all (where every map reads its default), are every case there is.
38
83
  const mapsIn = (t) => [...new Set([...JSON.stringify(t).matchAll(/"op":"get","map":"([^"]+)"/g)].map(x => x[1]))];
@@ -0,0 +1,17 @@
1
+ import { type DataLaw, type DataVerdict, type Options, type X } from './data.js';
2
+ type Op = {
3
+ name: string;
4
+ args: 1 | 2;
5
+ ret: 'nat' | 'bool';
6
+ x: (a: X, b: X) => X;
7
+ at: (a: number, b: number) => number;
8
+ };
9
+ export type Plan = {
10
+ bridges: Map<string, Op>;
11
+ unfolds: boolean;
12
+ };
13
+ export declare function transferPlan(l: DataLaw): Plan | undefined;
14
+ export declare function proveThrough(l: DataLaw, plan: Plan, opts: Options, until: number): (DataVerdict & {
15
+ tried: number;
16
+ }) | undefined;
17
+ export {};