gdp-wired 0.14.2 → 0.16.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.
@@ -0,0 +1,681 @@
1
+ // Invariants nobody wrote: a law that holds but isn't inductive, proven with laws the search finds. Nothing here is
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
4
+ // - candidates: linear forms Σ c·v ≤ d over the state and the params (an integer as its value, its two naturals
5
+ // together), up to three terms with small coefficients and the rule's own constants, each d the largest value the
6
+ // runs reached (and the rule's constants just above it); the same forms where a yes/no, a choice or a small natural
7
+ // holds one value (locked is 0 → …), and a choice's value no run reached as unreachable; and laws about every key
8
+ // of the maps: forms over the cells at one key (and a scalar), at every key or every key but a scalar's (k = owner
9
+ // or …), and where a cell holds one value
10
+ // - Houdini on the runs' neighbors: a reached state moved a little that every candidate alive and every law allows,
11
+ // stepped; a candidate its step breaks is in no inductive set of them, so it is dropped
12
+ // - the kernel: the laws with the candidates chosen so far, proven one law at a time; when a goal is stuck at a step,
13
+ // its witness is a counterexample to induction, so the candidate that rules it out (and the most of those seen) is
14
+ // added; a chosen candidate that nothing supports, or that makes a proof slow, is dropped; the set proven as a whole
15
+ // is the answer, after each chosen candidate the proof doesn't need is taken out again
16
+ import { any, atMost, evalC, get, interpret, n, not, plus, same, times, x } from "./machine.js";
17
+ import { proveRule, proveSteps } from "./dsl.js";
18
+ import { allowed, watch } from "./watch.js";
19
+ import { proveLaw, proveLaws, proveStepLaws } from "../kernel/prove.js";
20
+ const KEY = 'key$found', AT = '@', K = '#';
21
+ const seeded = (seed) => { let s = seed; return k => { s = (s * 1103515245 + 12345) % 2 ** 31; return Math.floor(s / 2 ** 31 * k); }; };
22
+ const isRule = (t) => typeof t.kindsOfState === 'function';
23
+ const gcd = (a, b) => (b === 0 ? Math.abs(a) : gcd(b, a % b));
24
+ // The constants a machine's expressions hold, and the factors it multiplies by.
25
+ function constantsOf(m, laws) {
26
+ const out = new Set();
27
+ const e = (t) => {
28
+ switch (t.op) {
29
+ case 'const':
30
+ out.add(t.value);
31
+ break;
32
+ case 'var': break;
33
+ case 'mul':
34
+ out.add(t.k);
35
+ e(t.a);
36
+ break;
37
+ case 'add':
38
+ case 'sub':
39
+ case 'min':
40
+ case 'max':
41
+ case 'prod':
42
+ e(t.a);
43
+ e(t.b);
44
+ break;
45
+ case 'if':
46
+ c(t.c);
47
+ e(t.then);
48
+ e(t.else);
49
+ break;
50
+ case 'get':
51
+ e(t.key);
52
+ break;
53
+ }
54
+ };
55
+ const c = (t) => {
56
+ switch (t.op) {
57
+ case '<':
58
+ case '<=':
59
+ case '=':
60
+ e(t.a);
61
+ e(t.b);
62
+ break;
63
+ case 'and':
64
+ case 'or':
65
+ t.xs.forEach(c);
66
+ break;
67
+ case 'not':
68
+ c(t.x);
69
+ break;
70
+ case 'flag': break;
71
+ case 'all':
72
+ c(t.c);
73
+ break;
74
+ }
75
+ };
76
+ Object.values(m.state).forEach(e);
77
+ Object.values(m.step.state ?? {}).forEach(e);
78
+ Object.values(m.step.flags ?? {}).forEach(c);
79
+ for (const w of Object.values(m.step.maps ?? {})) {
80
+ if ('key' in w)
81
+ e(w.key);
82
+ e(w.value);
83
+ }
84
+ if (m.assume)
85
+ c(m.assume);
86
+ if (m.pre)
87
+ c(m.pre);
88
+ laws.forEach(l => c(l.holds));
89
+ return [...out].filter(k => Number.isSafeInteger(k) && Math.abs(k) <= 1e6);
90
+ }
91
+ // A cell's key as the kernel names it (kernel.ts keyOf): `name:coef` parts joined by ',', '1' the constant.
92
+ const keyValue = (cell, w) => {
93
+ const inner = cell.slice(cell.indexOf('[') + 1, -1);
94
+ if (inner.includes('['))
95
+ return undefined;
96
+ let value = 0;
97
+ for (const part of inner.split(',').filter(Boolean)) {
98
+ const at = part.lastIndexOf(':'), name = part.slice(0, at), coef = Number(part.slice(at + 1));
99
+ if (name === '1')
100
+ value += coef;
101
+ else if (name in w)
102
+ value += coef * w[name];
103
+ else
104
+ return undefined;
105
+ }
106
+ return value;
107
+ };
108
+ export function inferLaws(target, laws, stepLaws = [], opts = {}) {
109
+ const t0 = performance.now(), until = t0 + (opts.ms ?? 20000), rule = isRule(target) ? target : undefined;
110
+ const m = rule ? rule.machine : target;
111
+ let proofs = 0;
112
+ const prove = (ls) => {
113
+ proofs++;
114
+ if (rule)
115
+ return stepLaws.length ? proveSteps(rule, ls, stepLaws, { search: false }) : proveRule(rule, ls, { search: false });
116
+ const out = stepLaws.length ? proveStepLaws(m, ls, stepLaws) : proveLaws(m, ls);
117
+ 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
+ };
119
+ const stats = (samples, candidates, alive) => ({ samples, candidates, alive, proofs, ms: Math.round(performance.now() - t0) });
120
+ const first = opts.first ?? prove(laws);
121
+ if ((first.proven && first.checked) || first.run)
122
+ return { ...first, found: [], stats: stats(0, 0, 0) };
123
+ // --- runs ------------------------------------------------------------------------------------------------------------
124
+ const run = interpret(m), rand = seeded(17), kinds = rule ? rule.kindsOfState() : [];
125
+ const given = rule ? [...rule.kindLaws().filter(k => !laws.some(l => l.name === k.name)), ...laws] : laws;
126
+ const randomParams = (bound) => (rule ? rule.randomParams(rand, bound) : Object.fromEntries(m.params.map(p => [p, rand(bound + 1)])));
127
+ const randomInput = (params, bound) => (rule ? rule.randomInput(rand, bound, params)
128
+ : { ...Object.fromEntries(m.inputs.map(i => [i, rand(bound + 1)])), ...Object.fromEntries((m.inputMaps ?? []).map(i => [i, new Map()])) });
129
+ const samples = [], cap = 3000;
130
+ const keep = (s) => { if (samples.length < cap)
131
+ samples.push(s);
132
+ else if (rand(4) === 0)
133
+ samples[rand(cap)] = s; };
134
+ for (const st of opts.seen ?? [])
135
+ if (!m.params.length)
136
+ keep({ params: {}, st });
137
+ const mapsText = (st) => JSON.stringify(Object.values(st.maps).map(c => [...c]));
138
+ const runsUntil = Math.min(until, performance.now() + (opts.runsMs ?? 500));
139
+ for (let trial = 0, bound = 2; performance.now() < runsUntil; trial++, bound = trial % 20 === 0 ? (bound >= 1024 ? 2 : bound * 2) : bound) {
140
+ const params = randomParams(bound);
141
+ if (m.pre && !evalC(m.pre, params, { scalars: {}, maps: {} }, m))
142
+ continue;
143
+ let st;
144
+ try {
145
+ st = run.init(params);
146
+ }
147
+ catch {
148
+ continue;
149
+ }
150
+ keep({ params, st });
151
+ for (let d = 0, tries = 0; d < 40 && tries < 200; tries++) {
152
+ const input = randomInput(params, bound);
153
+ let next;
154
+ try {
155
+ if (!allowed(m, run, params, st, input))
156
+ continue;
157
+ next = run.step(params, st, input);
158
+ }
159
+ catch {
160
+ break;
161
+ }
162
+ d++;
163
+ if (JSON.stringify(next.scalars) !== JSON.stringify(st.scalars) || mapsText(next) !== mapsText(st))
164
+ keep({ params, st: next });
165
+ st = next;
166
+ }
167
+ }
168
+ opts.progress?.('runs', { samples: samples.length });
169
+ // --- atoms and rows --------------------------------------------------------------------------------------------------
170
+ const scalars = Object.keys(m.state), stateSet = new Set(scalars), mapNames = Object.keys(m.maps ?? {}), mapSet = new Set(mapNames);
171
+ const atoms = [];
172
+ const add = (name, has, param, cell) => {
173
+ if (name.endsWith('$neg') && has(name.slice(0, -4)))
174
+ return;
175
+ const neg = `${name}$neg`, int = has(neg);
176
+ atoms.push({ name: cell ? `${name}[k]` : name, parts: int ? [[name, 1], [neg, -1]] : [[name, 1]], param, int, cell });
177
+ };
178
+ for (const name of scalars)
179
+ add(name, p => stateSet.has(p), false, false);
180
+ for (const name of m.params)
181
+ add(name, p => m.params.includes(p), true, false);
182
+ for (const name of mapNames)
183
+ add(name, p => mapSet.has(p), false, true);
184
+ const A = atoms.length, cellAtoms = atoms.flatMap((a, i) => (a.cell ? [i] : []));
185
+ const valueOf = (a, env) => { let s = 0; for (const [p, sign] of a.parts)
186
+ s += sign * (env[a.cell ? AT + p : p] ?? 0); return s; };
187
+ const cellOf = (st, map, key) => st.maps[map]?.get(key) ?? st.defaults?.[map] ?? m.maps[map];
188
+ // The scalar rows (one per state reached), and the cell rows (one per state and key a map holds, and one fresh key).
189
+ const keysIn = (st) => { const ks = new Set(); for (const c of Object.values(st.maps))
190
+ for (const k of c.keys())
191
+ ks.add(k); return [...ks]; };
192
+ const cellRows = (params, st) => {
193
+ if (!mapNames.length)
194
+ return [];
195
+ const ks = keysIn(st), fresh = ks.length ? Math.max(...ks) + 1 : 0;
196
+ return [...ks, fresh].map(k => ({ ...params, ...st.scalars, [K]: k, ...Object.fromEntries(mapNames.map(mp => [AT + mp, cellOf(st, mp, k)])) }));
197
+ };
198
+ const rowsS = samples.map(s => ({ ...s.params, ...s.st.scalars }));
199
+ const rowsK = [];
200
+ for (const s of samples) {
201
+ if (rowsK.length > 6000)
202
+ break;
203
+ rowsK.push(...cellRows(s.params, s.st));
204
+ }
205
+ const valsS = atoms.map(a => Float64Array.from(rowsS, r => valueOf(a, r))), valsK = atoms.map(a => Float64Array.from(rowsK, r => valueOf(a, r)));
206
+ const kindOf = new Map(kinds.map(([name, k]) => [name, k]));
207
+ const modes = [];
208
+ atoms.forEach((a, i) => {
209
+ if (a.param || a.int)
210
+ return;
211
+ const k = a.cell ? undefined : kindOf.get(a.name), seenValues = [...new Set(a.cell ? valsK[i] : valsS[i])].sort((p, q) => p - q);
212
+ const decoded = k?.vars(a.name).length === 1 ? k.decode(a.name, { [a.name]: 0 }) : undefined;
213
+ const values = k?.values;
214
+ if (typeof decoded === 'boolean')
215
+ modes.push({ atom: i, values: [0, 1], bool: true });
216
+ else if (typeof decoded === 'string' && values)
217
+ modes.push({ atom: i, values: values.map((_, j) => j), choice: values });
218
+ else if (!k && seenValues.length && seenValues.length <= 4 && seenValues.every(v => v <= 8))
219
+ modes.push({ atom: i, values: seenValues });
220
+ });
221
+ const modeOf = (i) => modes.find(mo => mo.atom === i);
222
+ // --- candidates ----------------------------------------------------------------------------------------------------
223
+ const consts = [...new Set([...constantsOf(m, laws), ...(opts.constants ?? [])].map(Math.abs))].filter(k => k > 0).sort((p, q) => p - q);
224
+ const mags = [...new Set([1, 2, 3, ...consts.filter(k => k > 3 && k <= 1000).slice(0, 4)])];
225
+ const bounds = [...new Set([...consts, ...consts.flatMap(a => consts.map(b => a * b)), ...consts.flatMap(a => consts.map(b => a + b))].flatMap(k => [k, -k])), 0].sort((p, q) => p - q);
226
+ const maxCandidates = opts.maxCandidates ?? 60000;
227
+ // The shapes: every coefficient vector over up to three atoms, coprime; a scalar form reads a state variable, a
228
+ // cell form a cell (and at most one scalar).
229
+ const shapes = [];
230
+ const pick = (terms, ms, pool, ok, cells) => {
231
+ const cs = ms.flatMap(k => [k, -k]);
232
+ const go = (from, acc) => {
233
+ if (shapes.length > maxCandidates)
234
+ return;
235
+ if (acc.length === terms) {
236
+ if (ok(acc) && acc.reduce((g, [, c]) => gcd(g, c), 0) === 1)
237
+ shapes.push({ coef: [...acc], cells });
238
+ return;
239
+ }
240
+ for (let i = from; i < pool.length; i++)
241
+ for (const c of cs)
242
+ go(i + 1, [...acc, [pool[i], c]]);
243
+ };
244
+ go(0, []);
245
+ };
246
+ const choose = (k, of) => { let r = 1; for (let i = 0; i < k; i++)
247
+ r = r * (of - i) / (i + 1); return r; };
248
+ // A choice's index orders its values arbitrarily: it is a guard, or alone (a bound on it reads as the values it
249
+ // allows), never a term among others.
250
+ const plain = atoms.flatMap((a, i) => (a.cell ? [] : [i])), terms = plain.filter(i => !modeOf(i)?.choice), P = terms.length;
251
+ const scalarOk = (acc) => acc.some(([i]) => !atoms[i].param);
252
+ pick(1, [1], plain, scalarOk, false);
253
+ pick(2, choose(2, P) * 4 * mags.length ** 2 <= maxCandidates / 4 ? mags : [1], terms, scalarOk, false);
254
+ if (choose(3, P) * 8 * mags.length ** 3 <= maxCandidates / 2)
255
+ pick(3, mags, terms, scalarOk, false);
256
+ else if (choose(3, P) * 8 <= maxCandidates / 2)
257
+ pick(3, [1], terms, scalarOk, false);
258
+ if (cellAtoms.length) {
259
+ 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;
260
+ pick(1, [1], cellAtoms, () => true, true);
261
+ pick(2, [1], all, cellOk, true);
262
+ if (choose(3, A) * 8 <= maxCandidates / 4)
263
+ pick(3, [1], all, acc => cellOk(acc) && acc.filter(([i]) => atoms[i].cell).length >= 2, true);
264
+ }
265
+ const sizeOf = (coef) => coef.length * 100 + coef.reduce((s, [, c]) => s + Math.abs(c), 0);
266
+ const forms = [];
267
+ const NS = rowsS.length, NK = rowsK.length;
268
+ const at = (coef, vals, j) => { let s = 0; for (const [i, c] of coef)
269
+ s += c * vals[i][j]; return s; };
270
+ const keyNots = plain.filter(i => !atoms[i].param && !atoms[i].int);
271
+ // The guards: the choices first, then yes/nos, then small naturals, at most eight; each shape's largest value per
272
+ // guard in one pass over (at most 1,500 of) the rows. Building candidates takes at most a quarter of the time.
273
+ const guards = [...modes].sort((p, q) => Number(!!q.choice) - Number(!!p.choice) || Number(!!q.bool) - Number(!!p.bool)).slice(0, 8);
274
+ const strideS = Math.max(1, Math.ceil(NS / 1500)), strideK = Math.max(1, Math.ceil(NK / 1500));
275
+ const buildUntil = Math.min(until, performance.now() + (until - performance.now()) / 4);
276
+ for (const [n_, { coef, cells }] of shapes.entries()) {
277
+ if (forms.length > maxCandidates || (n_ % 256 === 0 && performance.now() > buildUntil))
278
+ break;
279
+ const vals = cells ? valsK : valsS, N = cells ? NK : NS, rows = cells ? rowsK : rowsS;
280
+ let d = -Infinity;
281
+ for (let j = 0; j < N; j++)
282
+ d = Math.max(d, at(coef, vals, j));
283
+ if (!Number.isFinite(d))
284
+ continue;
285
+ forms.push({ coef, d, cells, size: sizeOf(coef) + (cells ? 10 : 0) });
286
+ // The rule's constants just above the largest value reached, a bound a run may not have hit.
287
+ for (const b of bounds.filter(b => b > d).slice(0, 2))
288
+ forms.push({ coef, d: b, cells, size: sizeOf(coef) + 1 + (cells ? 10 : 0) });
289
+ if (coef.length > 2)
290
+ continue;
291
+ const here = guards.filter(mode => !coef.some(([i]) => i === mode.atom) && (cells || !atoms[mode.atom].cell));
292
+ const most = here.map(() => new Map()), away = cells ? keyNots.map(() => -Infinity) : [];
293
+ for (let j = 0; j < N; j += cells ? strideK : strideS) {
294
+ const value = at(coef, vals, j);
295
+ here.forEach((mode, g) => { const v = vals[mode.atom][j], b = most[g]; b.set(v, Math.max(b.get(v) ?? -Infinity, value)); });
296
+ // At every key but a scalar's: the largest value away from that key.
297
+ if (cells)
298
+ keyNots.forEach((s, g) => { if (rows[j][K] !== valsK[s][j])
299
+ away[g] = Math.max(away[g], value); });
300
+ }
301
+ here.forEach((mode, g) => { for (const [value, dv] of most[g])
302
+ if (mode.values.includes(value) && dv < d)
303
+ forms.push({ coef, d: dv, cells, guard: { atom: mode.atom, value }, size: sizeOf(coef) + 50 }); });
304
+ keyNots.forEach((s, g) => { if (cells && Number.isFinite(away[g]) && away[g] < d)
305
+ forms.push({ coef, d: away[g], cells, guard: { keyNot: s }, size: sizeOf(coef) + 60 }); });
306
+ }
307
+ for (const mode of modes)
308
+ if (!atoms[mode.atom].cell)
309
+ for (const value of mode.values)
310
+ if (NS && !valsS[mode.atom].includes(value))
311
+ forms.push({ coef: [], d: 0, cells: false, unreachable: { atom: mode.atom, value }, size: 1 });
312
+ opts.progress?.('candidates', { atoms: A, shapes: shapes.length, forms: forms.length });
313
+ // --- how a form reads, and as a law ------------------------------------------------------------------------------------
314
+ const nameOf = (i) => atoms[i].name;
315
+ const shownValue = (i, value) => { const mo = modeOf(i); return mo?.choice ? (mo.choice[value] ?? String(value)) : mo?.bool ? (value === 1 ? 'yes' : 'no') : String(value); };
316
+ const sides = (f) => {
317
+ const term = ([i, c]) => `${Math.abs(c) === 1 ? '' : `${Math.abs(c)}·`}${nameOf(i)}`;
318
+ const left = f.coef.filter(([, c]) => c > 0), right = f.coef.filter(([, c]) => c < 0);
319
+ return { left, right, l: [...left.map(term), ...(f.d < 0 ? [String(-f.d)] : [])].join(' + ') || '0', r: [...right.map(term), ...(f.d > 0 ? [String(f.d)] : [])].join(' + ') || '0' };
320
+ };
321
+ // A bound on a choice alone reads as the values it allows.
322
+ const allowedValues = (f) => {
323
+ const only = f.coef.length === 1 ? modeOf(f.coef[0][0]) : undefined;
324
+ if (!only?.choice)
325
+ return undefined;
326
+ const c = f.coef[0][1];
327
+ return only.values.filter(v => c * v <= f.d);
328
+ };
329
+ const textOf = (f) => {
330
+ if (f.unreachable)
331
+ return `${nameOf(f.unreachable.atom)} is never ${shownValue(f.unreachable.atom, f.unreachable.value)}`;
332
+ const { l, r } = sides(f), within = allowedValues(f);
333
+ const body = within ? `${nameOf(f.coef[0][0])} is ${within.map(v => shownValue(f.coef[0][0], v)).join(' or ')}` : `${l} <= ${r}`;
334
+ const under = f.when ? (({ l: wl, r: wr }) => `${wl} <= ${wr} → `)(sides({ coef: f.when.coef, d: f.when.d, cells: false, size: 0 })) : '';
335
+ 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}`;
336
+ return f.cells ? `every k: ${guarded}` : guarded;
337
+ };
338
+ const sum = (es) => es.reduce((p, q) => plus(p, q), n(0));
339
+ const part = (a, p) => (a.cell ? get(p, x(KEY)) : x(p));
340
+ 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); }));
341
+ // Σ c·v ≤ d over naturals: the positive parts on the left, the negative ones and d on the right (an integer's
342
+ // negative part goes to the other side of its sign).
343
+ const condOf = (f) => {
344
+ // Not at a value, one-sided where it can be (less splitting for the kernel): a natural not 0 is at least 1, and a
345
+ // yes/no or a choice not at its last value is below it (its kind's law bounds it).
346
+ const notAt = (i, value) => {
347
+ const e = part(atoms[i], atoms[i].parts[0][0]), mo = modeOf(i), lastValue = mo?.bool ? 1 : mo?.choice ? mo.choice.length - 1 : undefined;
348
+ return value === 0 ? atMost(n(1), e) : value === lastValue ? atMost(e, n(value - 1)) : not(same(e, n(value)));
349
+ };
350
+ if (f.unreachable)
351
+ return notAt(f.unreachable.atom, f.unreachable.value);
352
+ const lhs = sum([...partsOf(f, 1), ...(f.d < 0 ? [n(-f.d)] : [])]), rhs = sum([...partsOf(f, -1), ...(f.d > 0 ? [n(f.d)] : [])]);
353
+ // Under a condition Σ ≤ d: its negation, −Σ ≤ −d − 1, or the body.
354
+ 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);
355
+ 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);
356
+ };
357
+ // The builder call, for a rule whose atoms are its own variables (naturals, or integers alone).
358
+ const sourceOf = (f, text) => {
359
+ if (!rule || f.when)
360
+ return undefined;
361
+ 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] : [])])];
362
+ const ints = f.coef.filter(([i]) => atoms[i].int).length;
363
+ if (ints && ints !== f.coef.length)
364
+ return undefined;
365
+ const ref = (i) => (atoms[i].cell ? `${atoms[i].parts[0][0]}.at(k)` : nameOf(i));
366
+ const val = (i) => { const mo = modeOf(i); return mo?.choice && !atoms[i].cell ? `${ref(i)}.index` : mo?.bool && !atoms[i].cell ? `${ref(i)}.n` : ref(i); };
367
+ const cond = (i, value) => { const mo = modeOf(i); return mo?.choice && !atoms[i].cell ? `${ref(i)}.is('${mo.choice[value]}')` : mo?.bool && !atoms[i].cell ? (value === 1 ? ref(i) : `${ref(i)}.not()`) : `${ref(i)}.eq(${value})`; };
368
+ const args = `({${[...new Set(used.map(i => (atoms[i].cell ? atoms[i].parts[0][0] : nameOf(i))))].join(', ')}})`;
369
+ const wrap = (body) => (f.cells ? `forall(k => ${body})` : body);
370
+ if (f.unreachable)
371
+ return `r.law('${text}', ${args} => ${cond(f.unreachable.atom, f.unreachable.value)}.not())`;
372
+ const term = (ts, k) => {
373
+ const parts = ts.map(([i, c]) => (Math.abs(c) === 1 ? val(i) : `${val(i)}.times(${Math.abs(c)})`));
374
+ if (!parts.length)
375
+ return String(k);
376
+ return parts.slice(1).reduce((acc, p) => `${acc}.plus(${p})`, parts[0]) + (k > 0 ? `.plus(${k})` : '');
377
+ };
378
+ const { left, right } = sides(f);
379
+ const body = left.length ? `${term(left, Math.max(0, -f.d))}.atMost(${term(right, Math.max(0, f.d))})` : `${term(right, Math.max(0, f.d))}.atLeast(${Math.max(0, -f.d)})`;
380
+ const guarded = !f.guard ? body : 'keyNot' in f.guard ? `k.eq(${nameOf(f.guard.keyNot)}).or(${body})` : `${cond(f.guard.atom, f.guard.value)}.implies(${body})`;
381
+ return `r.law('${text}', ${args} => ${wrap(guarded)})`;
382
+ };
383
+ // --- evaluating a form ---------------------------------------------------------------------------------------------
384
+ const holdsRow = (f, env) => {
385
+ if (f.unreachable)
386
+ return valueOf(atoms[f.unreachable.atom], env) !== f.unreachable.value;
387
+ if (f.guard) {
388
+ if ('keyNot' in f.guard) {
389
+ if (env[K] === valueOf(atoms[f.guard.keyNot], env))
390
+ return true;
391
+ }
392
+ else if (valueOf(atoms[f.guard.atom], env) !== f.guard.value)
393
+ return true;
394
+ }
395
+ if (f.when) {
396
+ let w = 0;
397
+ for (const [i, c] of f.when.coef)
398
+ w += c * valueOf(atoms[i], env);
399
+ if (w > f.when.d)
400
+ return true;
401
+ }
402
+ let s = 0;
403
+ for (const [i, c] of f.coef)
404
+ s += c * valueOf(atoms[i], env);
405
+ return s <= f.d;
406
+ };
407
+ const pointOf = (params, st) => ({ scalar: { ...params, ...st.scalars }, cells: cellRows(params, st) });
408
+ const holdsAt = (f, p) => (f.cells ? p.cells.every(r => holdsRow(f, r)) : holdsRow(f, p.scalar));
409
+ // A kernel witness as a point: its scalars, and a cell row per key it reads a map at.
410
+ const witnessPoint = (w) => {
411
+ const scalar = Object.fromEntries([...scalars, ...m.params].map(v => [v, w[v] ?? 0]));
412
+ const byKey = new Map();
413
+ for (const [name, value] of Object.entries(w)) {
414
+ const br = name.indexOf('[');
415
+ if (br < 0 || !name.endsWith(']') || !mapSet.has(name.slice(0, br)))
416
+ continue;
417
+ const key = keyValue(name, w);
418
+ if (key === undefined)
419
+ continue;
420
+ const row = byKey.get(key) ?? { ...scalar, [K]: key };
421
+ row[AT + name.slice(0, br)] = value;
422
+ byKey.set(key, row);
423
+ }
424
+ return { scalar, cells: [...byKey.values()] };
425
+ };
426
+ // --- Houdini on the runs' neighbors ----------------------------------------------------------------------------------
427
+ let alive = forms.slice().sort((p, q) => p.size - q.size).slice(0, maxCandidates);
428
+ const ctis = [];
429
+ const lawful = (params, st) => { try {
430
+ return given.every(l => watch(m, run, l, params, 4).start(st));
431
+ }
432
+ catch {
433
+ return false;
434
+ } };
435
+ const houdiniUntil = Math.min(until, performance.now() + Math.max(500, (until - performance.now()) / 4));
436
+ for (let quiet = 0; samples.length && performance.now() < houdiniUntil && quiet < 3;) {
437
+ const killed = new Set();
438
+ for (let tries = 0; tries < 400 && performance.now() < houdiniUntil; tries++) {
439
+ const base = samples[rand(samples.length)], scal = { ...base.st.scalars }, maps = { ...base.st.maps };
440
+ for (let k = 0, moves = 1 + rand(2); k < moves; k++) {
441
+ if (mapNames.length && rand(2) === 0) {
442
+ const mp = mapNames[rand(mapNames.length)], ks = keysIn(base.st), key = ks.length && rand(3) ? ks[rand(ks.length)] : rand(5);
443
+ const cells = new Map(maps[mp] ?? []);
444
+ cells.set(key, Math.max(0, cellOf(base.st, mp, key) + (rand(7) - 3)));
445
+ maps[mp] = cells;
446
+ }
447
+ else if (scalars.length) {
448
+ const v = scalars[rand(scalars.length)];
449
+ scal[v] = Math.max(0, scal[v] + (rand(7) - 3));
450
+ }
451
+ }
452
+ const st = { ...base.st, scalars: scal, maps }, pre = pointOf(base.params, st);
453
+ if (!alive.every(f => killed.has(f) || holdsAt(f, pre)) || !lawful(base.params, st))
454
+ continue;
455
+ const input = randomInput(base.params, 4);
456
+ let next;
457
+ try {
458
+ if (!allowed(m, run, base.params, st, input))
459
+ continue;
460
+ next = run.step(base.params, st, input);
461
+ }
462
+ catch {
463
+ continue;
464
+ }
465
+ const post = pointOf(base.params, next);
466
+ let broke = false;
467
+ for (const f of alive)
468
+ if (!killed.has(f) && !holdsAt(f, post)) {
469
+ killed.add(f);
470
+ broke = true;
471
+ }
472
+ if (!broke && !lawful(base.params, next))
473
+ ctis.push(pre);
474
+ }
475
+ quiet = killed.size ? 0 : quiet + 1;
476
+ alive = alive.filter(f => !killed.has(f));
477
+ }
478
+ opts.progress?.('houdini', { alive: alive.length, ctis: ctis.length });
479
+ // --- the kernel ----------------------------------------------------------------------------------------------------
480
+ const lawOf = new Map();
481
+ const lawFor = (f) => { let l = lawOf.get(f); if (!l)
482
+ lawOf.set(f, (l = { name: textOf(f), holds: condOf(f), ...(f.cells ? { forall: KEY } : {}) })); return l; };
483
+ const names = new Set(given.map(l => l.name));
484
+ alive = alive.filter(f => !names.has(textOf(f)));
485
+ let pool = alive.slice(), chosen = [];
486
+ const goalName = (g) => g.replace(/ @[^@]*$/, '');
487
+ let last = first;
488
+ const slow = Math.max(3000, (opts.ms ?? 20000) / 10);
489
+ // A point a form is false at; a cell form is judged only at rows that read every map it reads.
490
+ const reads = (f, r) => f.coef.every(([i]) => !atoms[i].cell || atoms[i].parts.every(([q]) => AT + q in r));
491
+ const falseAt = (f, p) => (f.cells ? p.cells.some(r => reads(f, r) && !holdsRow(f, r)) : !holdsRow(f, p.scalar));
492
+ const best = (p) => {
493
+ let pickF, score = -Infinity;
494
+ for (const f of pool) {
495
+ if (chosen.includes(f) || !falseAt(f, p))
496
+ continue;
497
+ const s = ctis.reduce((k, c) => k + (falseAt(f, c) ? 1 : 0), 0) * 1000 - f.size;
498
+ if (s > score)
499
+ (pickF = f, score = s);
500
+ }
501
+ return pickF;
502
+ };
503
+ // A counterexample to induction no candidate rules out: forms false at it that every sample meets under a condition
504
+ // it meets too (one value, or a sum or difference of two, at most a threshold: 0, ±1, or its own value there), the
505
+ // choice (loc, say) at its value there as the guard. A condition no sample meets gives its negation instead. Only the
506
+ // eight values most unusual at it are read (furthest past the samples in its guard, then the rarest there).
507
+ const choiceMode = modes.find(mo => mo.choice && !atoms[mo.atom].cell);
508
+ const condAtoms = plain.filter(i => !atoms[i].param && i !== choiceMode?.atom);
509
+ const fromCti = (p) => {
510
+ const stop = Math.min(until, performance.now() + Math.max(300, (until - performance.now()) / 8));
511
+ const sc = p.scalar, pv = (coef) => { let v = 0; for (const [i, c] of coef)
512
+ v += c * valueOf(atoms[i], sc); return v; };
513
+ const gv = choiceMode ? valueOf(atoms[choiceMode.atom], sc) : undefined;
514
+ const inGuard = [];
515
+ for (let j = 0; j < NS; j++)
516
+ if (gv === undefined || valsS[choiceMode.atom][j] === gv)
517
+ inGuard.push(j);
518
+ const unusual = (i) => {
519
+ const v = valueOf(atoms[i], sc);
520
+ let lo = Infinity, hi = -Infinity, same = 0;
521
+ for (const j of inGuard) {
522
+ const w = valsS[i][j];
523
+ if (w < lo)
524
+ lo = w;
525
+ if (w > hi)
526
+ hi = w;
527
+ if (w === v)
528
+ same++;
529
+ }
530
+ return [Math.max(0, lo - v, v - hi), -same];
531
+ };
532
+ 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);
533
+ // Over the focus: one or two values with the magnitudes, three with ±1.
534
+ const bodies = [];
535
+ const signed = (ks) => ks.flatMap(k => [k, -k]);
536
+ for (const [a, i] of focus.entries()) {
537
+ bodies.push({ coef: [[i, 1]] }, { coef: [[i, -1]] });
538
+ for (const [b2, k] of focus.slice(a + 1).entries()) {
539
+ for (const c1 of signed(mags))
540
+ for (const c2 of signed(mags))
541
+ if (gcd(c1, c2) === 1)
542
+ bodies.push({ coef: [[i, c1], [k, c2]] });
543
+ for (const q of focus.slice(a + b2 + 2))
544
+ for (const c1 of [1, -1])
545
+ for (const c2 of [1, -1])
546
+ for (const c3 of [1, -1])
547
+ bodies.push({ coef: [[i, c1], [k, c2], [q, c3]] });
548
+ }
549
+ }
550
+ const conds = [];
551
+ for (const [a, i] of focus.entries()) {
552
+ conds.push([[i, 1]], [[i, -1]]);
553
+ for (const k of focus.slice(a + 1))
554
+ conds.push([[i, 1], [k, -1]], [[i, -1], [k, 1]], [[i, 1], [k, 1]]);
555
+ }
556
+ const out = [], guard = choiceMode ? { atom: choiceMode.atom, value: gv } : undefined;
557
+ for (const coef of conds) {
558
+ const here = pv(coef);
559
+ for (const d of [...new Set([here, 0, 1, -1])].filter(d => here <= d)) {
560
+ if (performance.now() > stop)
561
+ break;
562
+ const rows = [];
563
+ for (const j of inGuard)
564
+ if (at(coef, valsS, j) <= d)
565
+ rows.push(j);
566
+ const when = { coef, d }, base = sizeOf(coef) + 80;
567
+ if (!rows.length) {
568
+ out.push({ coef: coef.map(([i, c]) => [i, -c]), d: -d - 1, cells: false, ...(guard ? { guard } : {}), size: base });
569
+ continue;
570
+ }
571
+ const stride = Math.max(1, Math.ceil(rows.length / 600));
572
+ for (const [q, sh] of bodies.entries()) {
573
+ if (q % 256 === 0 && performance.now() > stop)
574
+ break;
575
+ const v = pv(sh.coef);
576
+ let most = -Infinity, ok = true;
577
+ for (let t = 0; t < rows.length; t += stride) {
578
+ const w = at(sh.coef, valsS, rows[t]);
579
+ if (w >= v) {
580
+ ok = false;
581
+ break;
582
+ }
583
+ if (w > most)
584
+ most = w;
585
+ }
586
+ if (ok && Number.isFinite(most))
587
+ out.push({ coef: sh.coef, d: most, cells: false, ...(guard ? { guard } : {}), when, size: base + sizeOf(sh.coef) });
588
+ }
589
+ }
590
+ }
591
+ // A stride may skip a sample that breaks a form: each is checked again at every sample in its guard.
592
+ return out.sort((x, y) => x.size - y.size).slice(0, 2000).filter(f => inGuard.every(j => holdsRow(f, rowsS[j]))).slice(0, 400);
593
+ };
594
+ // The search proves one law at a time (proveLaw), each with every law assumed, and keeps what it proved while laws
595
+ // are only added (more laws assumed keep a proof a proof); dropping one forgets it all. Once every law is proven so,
596
+ // the set is proven as a whole, by the rule's own prover: that verdict is the answer.
597
+ // A law of the caller's (or a kind's) proven before any candidate was added stays proven whatever is added or dropped.
598
+ const done = new Set(), base = new Set();
599
+ const forget = () => { done.clear(); for (const b of base)
600
+ done.add(b); };
601
+ const drop = (f) => { chosen = chosen.filter(g => g !== f); pool = pool.filter(g => g !== f); forget(); };
602
+ const search = (ls) => {
603
+ for (let i = 0; i < ls.length; i++) {
604
+ if (done.has(ls[i].name))
605
+ continue;
606
+ proofs++;
607
+ const r = proveLaw(m, ls, i);
608
+ if (!r.proven)
609
+ return { proven: false, checked: false, goal: r.goal, stuck: r.stuck, ...(r.witness ? { witness: r.witness } : {}) };
610
+ done.add(ls[i].name);
611
+ if (!chosen.length)
612
+ base.add(ls[i].name);
613
+ }
614
+ if (stepLaws.length) {
615
+ proofs++;
616
+ const r = proveStepLaws(m, ls, stepLaws);
617
+ if (!r.proven)
618
+ return { proven: false, checked: false, ...(r.failed ? { goal: r.failed.goal, stuck: r.failed.stuck, ...(r.failed.witness ? { witness: r.failed.witness } : {}) } : {}) };
619
+ }
620
+ const whole = prove([...laws, ...chosen.map(lawFor)]);
621
+ if (!(whole.proven && whole.checked))
622
+ forget();
623
+ return whole;
624
+ };
625
+ for (let iter = 0; iter < 300 && performance.now() < until; iter++) {
626
+ const began = performance.now();
627
+ last = search([...given, ...chosen.map(lawFor)]);
628
+ const took = performance.now() - began;
629
+ opts.progress?.('proof', { chosen: chosen.length, goal: last.goal ?? '', stuck: last.stuck ?? '', ms: Math.round(took) });
630
+ if (last.proven && last.checked)
631
+ break;
632
+ if (!last.goal)
633
+ break;
634
+ const name = goalName(last.goal), stuckOn = chosen.find(f => lawFor(f).name === name);
635
+ if (last.run) {
636
+ // A real run breaks it: a candidate is false (dropped); a law of the caller's is false (the answer).
637
+ if (!stuckOn)
638
+ break;
639
+ drop(stuckOn);
640
+ continue;
641
+ }
642
+ // A proof the last candidate made slow (more cases to split) fails: that candidate goes.
643
+ if (chosen.length && took > slow) {
644
+ drop(chosen.at(-1));
645
+ continue;
646
+ }
647
+ const p = /@step|@\d+ steps/.test(last.goal) && last.witness ? witnessPoint(last.witness) : undefined;
648
+ if (p)
649
+ ctis.push(p);
650
+ let next = p ? best(p) : undefined;
651
+ if (!next && p) {
652
+ const t = performance.now(), made = fromCti(p);
653
+ pool.push(...made);
654
+ next = best(p);
655
+ opts.progress?.('cti', { forms: made.length, ms: Math.round(performance.now() - t) });
656
+ }
657
+ if (next) {
658
+ chosen.push(next);
659
+ continue;
660
+ }
661
+ // Stuck with no counterexample on a goal not a candidate's: the last candidate made it harder, so it goes.
662
+ if (chosen.length && !stuckOn && !p) {
663
+ drop(chosen.at(-1));
664
+ continue;
665
+ }
666
+ if (!stuckOn)
667
+ break;
668
+ drop(stuckOn);
669
+ }
670
+ if (!(last.proven && last.checked))
671
+ return { ...last, found: [], stats: stats(samples.length, forms.length, alive.length) };
672
+ // Take out what the proof doesn't need, the last added first.
673
+ for (const f of [...chosen].reverse()) {
674
+ if (performance.now() > until)
675
+ break;
676
+ const without = chosen.filter(g => g !== f), v = prove([...laws, ...without.map(lawFor)]);
677
+ if (v.proven && v.checked)
678
+ (chosen = without, last = v);
679
+ }
680
+ 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) };
681
+ }