gdp-wired 0.14.1 → 0.15.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,573 @@
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) : proveRule(rule, ls);
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
+ const plain = atoms.flatMap((a, i) => (a.cell ? [] : [i])), P = plain.length;
249
+ const scalarOk = (acc) => acc.some(([i]) => !atoms[i].param);
250
+ pick(1, [1], plain, scalarOk, false);
251
+ pick(2, choose(2, P) * 4 * mags.length ** 2 <= maxCandidates / 4 ? mags : [1], plain, scalarOk, false);
252
+ if (choose(3, P) * 8 * mags.length ** 3 <= maxCandidates / 2)
253
+ pick(3, mags, plain, scalarOk, false);
254
+ else if (choose(3, P) * 8 <= maxCandidates / 2)
255
+ pick(3, [1], plain, scalarOk, false);
256
+ 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;
258
+ pick(1, [1], cellAtoms, () => true, true);
259
+ pick(2, [1], all, cellOk, true);
260
+ if (choose(3, A) * 8 <= maxCandidates / 4)
261
+ pick(3, [1], all, acc => cellOk(acc) && acc.filter(([i]) => atoms[i].cell).length >= 2, true);
262
+ }
263
+ const sizeOf = (coef) => coef.length * 100 + coef.reduce((s, [, c]) => s + Math.abs(c), 0);
264
+ const forms = [];
265
+ const NS = rowsS.length, NK = rowsK.length;
266
+ const at = (coef, vals, j) => { let s = 0; for (const [i, c] of coef)
267
+ s += c * vals[i][j]; return s; };
268
+ const keyNots = plain.filter(i => !atoms[i].param && !atoms[i].int);
269
+ // The guards: the choices first, then yes/nos, then small naturals, at most eight; each shape's largest value per
270
+ // guard in one pass over (at most 1,500 of) the rows. Building candidates takes at most a quarter of the time.
271
+ const guards = [...modes].sort((p, q) => Number(!!q.choice) - Number(!!p.choice) || Number(!!q.bool) - Number(!!p.bool)).slice(0, 8);
272
+ const strideS = Math.max(1, Math.ceil(NS / 1500)), strideK = Math.max(1, Math.ceil(NK / 1500));
273
+ const buildUntil = Math.min(until, performance.now() + (until - performance.now()) / 4);
274
+ for (const [n_, { coef, cells }] of shapes.entries()) {
275
+ if (forms.length > maxCandidates || (n_ % 256 === 0 && performance.now() > buildUntil))
276
+ break;
277
+ const vals = cells ? valsK : valsS, N = cells ? NK : NS, rows = cells ? rowsK : rowsS;
278
+ let d = -Infinity;
279
+ for (let j = 0; j < N; j++)
280
+ d = Math.max(d, at(coef, vals, j));
281
+ if (!Number.isFinite(d))
282
+ continue;
283
+ forms.push({ coef, d, cells, size: sizeOf(coef) + (cells ? 10 : 0) });
284
+ // The rule's constants just above the largest value reached, a bound a run may not have hit.
285
+ for (const b of bounds.filter(b => b > d).slice(0, 2))
286
+ forms.push({ coef, d: b, cells, size: sizeOf(coef) + 1 + (cells ? 10 : 0) });
287
+ if (coef.length > 2)
288
+ continue;
289
+ const here = guards.filter(mode => !coef.some(([i]) => i === mode.atom) && (cells || !atoms[mode.atom].cell));
290
+ const most = here.map(() => new Map()), away = cells ? keyNots.map(() => -Infinity) : [];
291
+ for (let j = 0; j < N; j += cells ? strideK : strideS) {
292
+ const value = at(coef, vals, j);
293
+ here.forEach((mode, g) => { const v = vals[mode.atom][j], b = most[g]; b.set(v, Math.max(b.get(v) ?? -Infinity, value)); });
294
+ // At every key but a scalar's: the largest value away from that key.
295
+ if (cells)
296
+ keyNots.forEach((s, g) => { if (rows[j][K] !== valsK[s][j])
297
+ away[g] = Math.max(away[g], value); });
298
+ }
299
+ here.forEach((mode, g) => { for (const [value, dv] of most[g])
300
+ if (mode.values.includes(value) && dv < d)
301
+ forms.push({ coef, d: dv, cells, guard: { atom: mode.atom, value }, size: sizeOf(coef) + 50 }); });
302
+ keyNots.forEach((s, g) => { if (cells && Number.isFinite(away[g]) && away[g] < d)
303
+ forms.push({ coef, d: away[g], cells, guard: { keyNot: s }, size: sizeOf(coef) + 60 }); });
304
+ }
305
+ for (const mode of modes)
306
+ if (!atoms[mode.atom].cell)
307
+ for (const value of mode.values)
308
+ if (NS && !valsS[mode.atom].includes(value))
309
+ forms.push({ coef: [], d: 0, cells: false, unreachable: { atom: mode.atom, value }, size: 1 });
310
+ opts.progress?.('candidates', { atoms: A, shapes: shapes.length, forms: forms.length });
311
+ // --- how a form reads, and as a law ------------------------------------------------------------------------------------
312
+ const nameOf = (i) => atoms[i].name;
313
+ const shownValue = (i, value) => { const mo = modeOf(i); return mo?.choice ? (mo.choice[value] ?? String(value)) : mo?.bool ? (value === 1 ? 'yes' : 'no') : String(value); };
314
+ const sides = (f) => {
315
+ const term = ([i, c]) => `${Math.abs(c) === 1 ? '' : `${Math.abs(c)}·`}${nameOf(i)}`;
316
+ const left = f.coef.filter(([, c]) => c > 0), right = f.coef.filter(([, c]) => c < 0);
317
+ 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' };
318
+ };
319
+ // A bound on a choice alone reads as the values it allows.
320
+ const allowedValues = (f) => {
321
+ const only = f.coef.length === 1 ? modeOf(f.coef[0][0]) : undefined;
322
+ if (!only?.choice)
323
+ return undefined;
324
+ const c = f.coef[0][1];
325
+ return only.values.filter(v => c * v <= f.d);
326
+ };
327
+ const textOf = (f) => {
328
+ if (f.unreachable)
329
+ return `${nameOf(f.unreachable.atom)} is never ${shownValue(f.unreachable.atom, f.unreachable.value)}`;
330
+ const { l, r } = sides(f), within = allowedValues(f);
331
+ 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}`;
333
+ return f.cells ? `every k: ${guarded}` : guarded;
334
+ };
335
+ const sum = (es) => es.reduce((p, q) => plus(p, q), n(0));
336
+ const part = (a, p) => (a.cell ? get(p, x(KEY)) : x(p));
337
+ 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); }));
338
+ // Σ c·v ≤ d over naturals: the positive parts on the left, the negative ones and d on the right (an integer's
339
+ // negative part goes to the other side of its sign).
340
+ const condOf = (f) => {
341
+ // 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
342
+ // yes/no or a choice not at its last value is below it (its kind's law bounds it).
343
+ const notAt = (i, value) => {
344
+ const e = part(atoms[i], atoms[i].parts[0][0]), mo = modeOf(i), lastValue = mo?.bool ? 1 : mo?.choice ? mo.choice.length - 1 : undefined;
345
+ return value === 0 ? atMost(n(1), e) : value === lastValue ? atMost(e, n(value - 1)) : not(same(e, n(value)));
346
+ };
347
+ if (f.unreachable)
348
+ return notAt(f.unreachable.atom, f.unreachable.value);
349
+ 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);
351
+ 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
+ };
353
+ // The builder call, for a rule whose atoms are its own variables (naturals, or integers alone).
354
+ const sourceOf = (f, text) => {
355
+ if (!rule)
356
+ return undefined;
357
+ 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
+ const ints = f.coef.filter(([i]) => atoms[i].int).length;
359
+ if (ints && ints !== f.coef.length)
360
+ return undefined;
361
+ const ref = (i) => (atoms[i].cell ? `${atoms[i].parts[0][0]}.at(k)` : nameOf(i));
362
+ 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); };
363
+ 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})`; };
364
+ const args = `({${[...new Set(used.map(i => (atoms[i].cell ? atoms[i].parts[0][0] : nameOf(i))))].join(', ')}})`;
365
+ const wrap = (body) => (f.cells ? `forall(k => ${body})` : body);
366
+ if (f.unreachable)
367
+ return `r.law('${text}', ${args} => ${cond(f.unreachable.atom, f.unreachable.value)}.not())`;
368
+ const term = (ts, k) => {
369
+ const parts = ts.map(([i, c]) => (Math.abs(c) === 1 ? val(i) : `${val(i)}.times(${Math.abs(c)})`));
370
+ if (!parts.length)
371
+ return String(k);
372
+ return parts.slice(1).reduce((acc, p) => `${acc}.plus(${p})`, parts[0]) + (k > 0 ? `.plus(${k})` : '');
373
+ };
374
+ const { left, right } = sides(f);
375
+ 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)})`;
376
+ 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})`;
377
+ return `r.law('${text}', ${args} => ${wrap(guarded)})`;
378
+ };
379
+ // --- evaluating a form ---------------------------------------------------------------------------------------------
380
+ const holdsRow = (f, env) => {
381
+ if (f.unreachable)
382
+ return valueOf(atoms[f.unreachable.atom], env) !== f.unreachable.value;
383
+ if (f.guard) {
384
+ if ('keyNot' in f.guard) {
385
+ if (env[K] === valueOf(atoms[f.guard.keyNot], env))
386
+ return true;
387
+ }
388
+ else if (valueOf(atoms[f.guard.atom], env) !== f.guard.value)
389
+ return true;
390
+ }
391
+ let s = 0;
392
+ for (const [i, c] of f.coef)
393
+ s += c * valueOf(atoms[i], env);
394
+ return s <= f.d;
395
+ };
396
+ const pointOf = (params, st) => ({ scalar: { ...params, ...st.scalars }, cells: cellRows(params, st) });
397
+ const holdsAt = (f, p) => (f.cells ? p.cells.every(r => holdsRow(f, r)) : holdsRow(f, p.scalar));
398
+ // A kernel witness as a point: its scalars, and a cell row per key it reads a map at.
399
+ const witnessPoint = (w) => {
400
+ const scalar = Object.fromEntries([...scalars, ...m.params].map(v => [v, w[v] ?? 0]));
401
+ const byKey = new Map();
402
+ for (const [name, value] of Object.entries(w)) {
403
+ const br = name.indexOf('[');
404
+ if (br < 0 || !name.endsWith(']') || !mapSet.has(name.slice(0, br)))
405
+ continue;
406
+ const key = keyValue(name, w);
407
+ if (key === undefined)
408
+ continue;
409
+ const row = byKey.get(key) ?? { ...scalar, [K]: key };
410
+ row[AT + name.slice(0, br)] = value;
411
+ byKey.set(key, row);
412
+ }
413
+ return { scalar, cells: [...byKey.values()] };
414
+ };
415
+ // --- Houdini on the runs' neighbors ----------------------------------------------------------------------------------
416
+ let alive = forms.slice().sort((p, q) => p.size - q.size).slice(0, maxCandidates);
417
+ const ctis = [];
418
+ const lawful = (params, st) => { try {
419
+ return given.every(l => watch(m, run, l, params, 4).start(st));
420
+ }
421
+ catch {
422
+ return false;
423
+ } };
424
+ const houdiniUntil = Math.min(until, performance.now() + Math.max(500, (until - performance.now()) / 4));
425
+ for (let quiet = 0; samples.length && performance.now() < houdiniUntil && quiet < 3;) {
426
+ const killed = new Set();
427
+ for (let tries = 0; tries < 400 && performance.now() < houdiniUntil; tries++) {
428
+ const base = samples[rand(samples.length)], scal = { ...base.st.scalars }, maps = { ...base.st.maps };
429
+ for (let k = 0, moves = 1 + rand(2); k < moves; k++) {
430
+ if (mapNames.length && rand(2) === 0) {
431
+ const mp = mapNames[rand(mapNames.length)], ks = keysIn(base.st), key = ks.length && rand(3) ? ks[rand(ks.length)] : rand(5);
432
+ const cells = new Map(maps[mp] ?? []);
433
+ cells.set(key, Math.max(0, cellOf(base.st, mp, key) + (rand(7) - 3)));
434
+ maps[mp] = cells;
435
+ }
436
+ else if (scalars.length) {
437
+ const v = scalars[rand(scalars.length)];
438
+ scal[v] = Math.max(0, scal[v] + (rand(7) - 3));
439
+ }
440
+ }
441
+ const st = { ...base.st, scalars: scal, maps }, pre = pointOf(base.params, st);
442
+ if (!alive.every(f => killed.has(f) || holdsAt(f, pre)) || !lawful(base.params, st))
443
+ continue;
444
+ const input = randomInput(base.params, 4);
445
+ let next;
446
+ try {
447
+ if (!allowed(m, run, base.params, st, input))
448
+ continue;
449
+ next = run.step(base.params, st, input);
450
+ }
451
+ catch {
452
+ continue;
453
+ }
454
+ const post = pointOf(base.params, next);
455
+ let broke = false;
456
+ for (const f of alive)
457
+ if (!killed.has(f) && !holdsAt(f, post)) {
458
+ killed.add(f);
459
+ broke = true;
460
+ }
461
+ if (!broke && !lawful(base.params, next))
462
+ ctis.push(pre);
463
+ }
464
+ quiet = killed.size ? 0 : quiet + 1;
465
+ alive = alive.filter(f => !killed.has(f));
466
+ }
467
+ opts.progress?.('houdini', { alive: alive.length, ctis: ctis.length });
468
+ // --- the kernel ----------------------------------------------------------------------------------------------------
469
+ const lawOf = new Map();
470
+ const lawFor = (f) => { let l = lawOf.get(f); if (!l)
471
+ lawOf.set(f, (l = { name: textOf(f), holds: condOf(f), ...(f.cells ? { forall: KEY } : {}) })); return l; };
472
+ const names = new Set(given.map(l => l.name));
473
+ alive = alive.filter(f => !names.has(textOf(f)));
474
+ let pool = alive.slice(), chosen = [];
475
+ const goalName = (g) => g.replace(/ @[^@]*$/, '');
476
+ let last = first;
477
+ const slow = Math.max(3000, (opts.ms ?? 20000) / 10);
478
+ // A point a form is false at; a cell form is judged only at rows that read every map it reads.
479
+ const reads = (f, r) => f.coef.every(([i]) => !atoms[i].cell || atoms[i].parts.every(([q]) => AT + q in r));
480
+ const falseAt = (f, p) => (f.cells ? p.cells.some(r => reads(f, r) && !holdsRow(f, r)) : !holdsRow(f, p.scalar));
481
+ const best = (p) => {
482
+ let pickF, score = -Infinity;
483
+ for (const f of pool) {
484
+ if (chosen.includes(f) || !falseAt(f, p))
485
+ continue;
486
+ const s = ctis.reduce((k, c) => k + (falseAt(f, c) ? 1 : 0), 0) * 1000 - f.size;
487
+ if (s > score)
488
+ (pickF = f, score = s);
489
+ }
490
+ return pickF;
491
+ };
492
+ // The search proves one law at a time (proveLaw), each with every law assumed, and keeps what it proved while laws
493
+ // are only added (more laws assumed keep a proof a proof); dropping one forgets it all. Once every law is proven so,
494
+ // the set is proven as a whole, by the rule's own prover: that verdict is the answer.
495
+ // A law of the caller's (or a kind's) proven before any candidate was added stays proven whatever is added or dropped.
496
+ const done = new Set(), base = new Set();
497
+ const forget = () => { done.clear(); for (const b of base)
498
+ done.add(b); };
499
+ const drop = (f) => { chosen = chosen.filter(g => g !== f); pool = pool.filter(g => g !== f); forget(); };
500
+ const search = (ls) => {
501
+ for (let i = 0; i < ls.length; i++) {
502
+ if (done.has(ls[i].name))
503
+ continue;
504
+ proofs++;
505
+ const r = proveLaw(m, ls, i);
506
+ if (!r.proven)
507
+ return { proven: false, checked: false, goal: r.goal, stuck: r.stuck, ...(r.witness ? { witness: r.witness } : {}) };
508
+ done.add(ls[i].name);
509
+ if (!chosen.length)
510
+ base.add(ls[i].name);
511
+ }
512
+ if (stepLaws.length) {
513
+ proofs++;
514
+ const r = proveStepLaws(m, ls, stepLaws);
515
+ if (!r.proven)
516
+ return { proven: false, checked: false, ...(r.failed ? { goal: r.failed.goal, stuck: r.failed.stuck, ...(r.failed.witness ? { witness: r.failed.witness } : {}) } : {}) };
517
+ }
518
+ const whole = prove([...laws, ...chosen.map(lawFor)]);
519
+ if (!(whole.proven && whole.checked))
520
+ forget();
521
+ return whole;
522
+ };
523
+ for (let iter = 0; iter < 300 && performance.now() < until; iter++) {
524
+ const began = performance.now();
525
+ last = search([...given, ...chosen.map(lawFor)]);
526
+ const took = performance.now() - began;
527
+ opts.progress?.('proof', { chosen: chosen.length, goal: last.goal ?? '', stuck: last.stuck ?? '', ms: Math.round(took) });
528
+ if (last.proven && last.checked)
529
+ break;
530
+ if (!last.goal)
531
+ break;
532
+ const name = goalName(last.goal), stuckOn = chosen.find(f => lawFor(f).name === name);
533
+ if (last.run) {
534
+ // A real run breaks it: a candidate is false (dropped); a law of the caller's is false (the answer).
535
+ if (!stuckOn)
536
+ break;
537
+ drop(stuckOn);
538
+ continue;
539
+ }
540
+ // A proof the last candidate made slow (more cases to split) fails: that candidate goes.
541
+ if (chosen.length && took > slow) {
542
+ drop(chosen.at(-1));
543
+ continue;
544
+ }
545
+ const p = /@step|@\d+ steps/.test(last.goal) && last.witness ? witnessPoint(last.witness) : undefined;
546
+ if (p)
547
+ ctis.push(p);
548
+ const next = p ? best(p) : undefined;
549
+ if (next) {
550
+ chosen.push(next);
551
+ continue;
552
+ }
553
+ // Stuck with no counterexample on a goal not a candidate's: the last candidate made it harder, so it goes.
554
+ if (chosen.length && !stuckOn && !p) {
555
+ drop(chosen.at(-1));
556
+ continue;
557
+ }
558
+ if (!stuckOn)
559
+ break;
560
+ drop(stuckOn);
561
+ }
562
+ if (!(last.proven && last.checked))
563
+ return { ...last, found: [], stats: stats(samples.length, forms.length, alive.length) };
564
+ // Take out what the proof doesn't need, the last added first.
565
+ for (const f of [...chosen].reverse()) {
566
+ if (performance.now() > until)
567
+ break;
568
+ const without = chosen.filter(g => g !== f), v = prove([...laws, ...without.map(lawFor)]);
569
+ if (v.proven && v.checked)
570
+ (chosen = without, last = v);
571
+ }
572
+ 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) };
573
+ }
@@ -8,6 +8,7 @@ export type Tree = {
8
8
  } | {
9
9
  leaf: Step[];
10
10
  products?: [number, number][];
11
+ squares?: Lin[];
11
12
  };
12
13
  export type Goal = {
13
14
  name: string;
@@ -25,6 +26,9 @@ export declare const termColumn: (e: E) => string;
25
26
  export declare const columnTerm: (name: string) => E | undefined;
26
27
  export declare function decided(e: E, path: Path): boolean;
27
28
  export declare function termRows(name: string, path: Path): Lin[];
29
+ export declare function factorsOf(name: string): string[];
30
+ export declare const isMonomial: (name: string) => boolean;
31
+ export declare const isCell: (name: string) => boolean;
28
32
  export declare function truth(c: C, path: Path): boolean | undefined;
29
33
  export declare function facts(c: C, path: Path, positive?: boolean): Lin[];
30
34
  export declare function leafConstraints(goal: Goal, path: Path): Constraint[];
@@ -43,7 +47,7 @@ export type Seen<T> = {
43
47
  size?: number;
44
48
  };
45
49
  export declare function reusing<T>(seen: Seen<T>, path: Path, f: (path: Path) => T): T;
46
- export declare function withProducts(rows: Constraint[], pairs: [number, number][]): Constraint[];
50
+ export declare function withProducts(rows: Constraint[], pairs: [number, number][], squares?: Lin[]): Constraint[];
47
51
  export declare function check(goal: Goal, tree: Tree, path?: Path): boolean;
48
52
  export type Subst = {
49
53
  vars: Record<string, E>;