gdp-wired 0.10.0 → 0.11.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.
@@ -130,7 +130,12 @@ export function evalE(e, env, st, m) {
130
130
  case 'max': return Math.max(evalE(e.a, env, st, m), evalE(e.b, env, st, m));
131
131
  case 'if': return evalC(e.c, env, st, m) ? evalE(e.then, env, st, m) : evalE(e.else, env, st, m);
132
132
  case 'get': {
133
- const key = evalE(e.key, env, st, m), fn = m.fns?.[e.map];
133
+ const fn = m.fns?.[e.map], args = fn?.call ? pairedArgs(e.key, fn.arity) : undefined;
134
+ if (args) {
135
+ const xs = args.map(a => evalE(a, env, st, m));
136
+ return naturalIn(callOf(fn)(...xs), `${e.map}(${xs.join(', ')})`);
137
+ }
138
+ const key = evalE(e.key, env, st, m);
134
139
  return fn ? naturalIn(fnOf(fn)(key), `${e.map}(${key})`) : st.maps[e.map].get(key) ?? st.defaults?.[e.map] ?? m.maps[e.map];
135
140
  }
136
141
  }
@@ -147,6 +152,34 @@ const fnOf = (fn) => {
147
152
  }
148
153
  return f;
149
154
  };
155
+ const compiledCalls = new WeakMap();
156
+ const callOf = (fn) => {
157
+ let f = compiledCalls.get(fn);
158
+ if (!f) {
159
+ f = compileFunction([], `return (${fn.call})`)();
160
+ compiledCalls.set(fn, f);
161
+ }
162
+ return f;
163
+ };
164
+ // The n arguments a key pairs (left-nested, ((a, b), c)..., each pair (s)² + b with s = a + b), if it is their pairing.
165
+ const paired = new WeakMap();
166
+ export function pairedArgs(key, n) {
167
+ let byN = paired.get(key);
168
+ if (!byN)
169
+ paired.set(key, (byN = new Map()));
170
+ if (byN.has(n))
171
+ return byN.get(n);
172
+ let out;
173
+ if (n === 1)
174
+ out = [key];
175
+ else if (key.op === 'add' && key.a.op === 'prod' && key.a.a.op === 'add' && alike(key.a.a, key.a.b) && alike(key.a.a.b, key.b)) {
176
+ const first = pairedArgs(key.a.a.a, n - 1);
177
+ out = first && [...first, key.b];
178
+ }
179
+ byN.set(n, out);
180
+ return out;
181
+ }
182
+ const alike = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
150
183
  export function evalC(c, env, st, m) {
151
184
  switch (c.op) {
152
185
  case '<': return evalE(c.a, env, st, m) < evalE(c.b, env, st, m);
@@ -323,7 +356,12 @@ export function compileJs(m) {
323
356
  case 'min': return `Math.min(${ex(e.a)}, ${ex(e.b)})`;
324
357
  case 'max': return `Math.max(${ex(e.a)}, ${ex(e.b)})`;
325
358
  case 'if': return `(${cx(e.c)} ? ${ex(e.then)} : ${ex(e.else)})`;
326
- case 'get': return fns.includes(e.map) ? `$n(fn${fns.indexOf(e.map)}(${ex(e.key)}), ${id(`${e.map}()`)})` : `(maps[${id(e.map)}].get(${ex(e.key)}) ?? ${carriedMaps.includes(e.map) ? '0' : `dft[${id(e.map)}] ?? ${plain.maps[e.map]}`})`;
359
+ case 'get': {
360
+ const fn = m.fns?.[e.map], args = fn?.call ? pairedArgs(e.key, fn.arity) : undefined;
361
+ if (args)
362
+ return `$n(fnc${fns.indexOf(e.map)}(${args.map(ex).join(', ')}), ${id(`${e.map}()`)})`;
363
+ return fns.includes(e.map) ? `$n(fn${fns.indexOf(e.map)}(${ex(e.key)}), ${id(`${e.map}()`)})` : `(maps[${id(e.map)}].get(${ex(e.key)}) ?? ${carriedMaps.includes(e.map) ? '0' : `dft[${id(e.map)}] ?? ${plain.maps[e.map]}`})`;
364
+ }
327
365
  }
328
366
  };
329
367
  const cx = (c) => {
@@ -379,7 +417,7 @@ function $n(v, what) { if (typeof v !== 'number' || !Number.isSafeInteger(v) ||
379
417
  function $x(v) { if (v > 9007199254740991) throw new RangeError('a value passed 2^53 - 1, past which the proofs over the naturals no longer describe the run'); return v; }
380
418
  function $m(v, what) { if (!(v instanceof Map)) throw new RangeError(what + ' must be a Map of naturals'); for (const [key, x] of v) { $n(key, 'a key of ' + what); $n(x, 'a cell of ' + what); } return v; }
381
419
  function $keys(maps, names) { const keys = new Set(); let past = 0; for (const c of Object.values(maps)) for (const key of c.keys()) past = Math.max(past, key + 1); for (const n of names) for (const key of maps[n].keys()) keys.add(key); return [...keys, past]; }
382
- ${fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n`).join('')}function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }
420
+ ${fns.map((name, i) => `const fn${i} = (${m.fns[name].js});\n${m.fns[name].call ? `const fnc${i} = (${m.fns[name].call});\n` : ''}`).join('')}function $breaks(maps, reads, cmps) { const p = new Set([0]); for (const [n, d, minus] of reads) { for (const s of maps[n].keys()) { if (minus) p.add(s + d); else if (s >= d) p.add(s - d); } if (minus) p.add(d); } for (const [v, d, minus] of cmps) { if (minus) { p.add(v + d); p.add(d); } else if (v >= d) p.add(v - d); } const keys = new Set(); for (const q of p) for (let i = -1; i <= 2; i++) if (q + i >= 0) keys.add(q + i); const sorted = [...keys].sort((a, b) => a - b); sorted.push(sorted[sorted.length - 1] + 1); return sorted; }
383
421
  function $dense(keys, f) { const top = keys[keys.length - 1]; if (top > 1000000) throw new RangeError('a write at every key reads its key in arithmetic past a million keys (' + top + ')'); const d = f(top), cells = new Map(); for (let k = 0; k < top; k++) { const v = f(k); if (v !== d) cells.set(k, v); } return {cells, d}; }
384
422
  function $every(keys, f) { const past = keys[keys.length - 1], d = f(past), cells = new Map(); for (let i = 0; i < keys.length - 1; i++) { const v = f(keys[i]); if (v !== d) cells.set(keys[i], v); } return {cells, d}; }
385
423
  export function init(params) {
@@ -39,3 +39,4 @@ export type Verdict = {
39
39
  point?: Record<string, number>;
40
40
  };
41
41
  export declare function solve(constraints: Constraint[], maxRows?: number): Verdict;
42
+ export declare function refuteFast(constraints: Constraint[], maxRows?: number): Verdict | undefined;
@@ -8,18 +8,38 @@
8
8
  // exact shadow). Then a rational solution of the rest extends to an integer x, so back-substitution finds a witness.
9
9
  export const v = (name) => ({ [name]: 1 });
10
10
  export const k = (value) => (value === 0 ? {} : { 1: value });
11
+ // (Plain loops, no intermediate arrays: these run millions of times in a proof. Each keeps the keys in the order they
12
+ // first appear, as a sum over the entries would.)
11
13
  export function add(...xs) {
12
14
  const out = {};
13
15
  for (const x of xs)
14
- for (const [key, value] of Object.entries(x))
15
- out[key] = (out[key] ?? 0) + value;
16
- for (const key of Object.keys(out))
16
+ for (const key in x)
17
+ out[key] = (out[key] ?? 0) + x[key];
18
+ for (const key in out)
17
19
  if (out[key] === 0)
18
20
  delete out[key];
19
21
  return out;
20
22
  }
21
- export const scale = (x, f) => add(Object.fromEntries(Object.entries(x).map(([key, value]) => [key, value * f])));
22
- export const sub = (a, b) => add(a, scale(b, -1));
23
+ export const scale = (x, f) => {
24
+ const out = {};
25
+ for (const key in x) {
26
+ const value = x[key] * f;
27
+ if (value !== 0)
28
+ out[key] = value;
29
+ }
30
+ return out;
31
+ };
32
+ export const sub = (a, b) => {
33
+ const out = {};
34
+ for (const key in a)
35
+ out[key] = a[key];
36
+ for (const key in b)
37
+ out[key] = (out[key] ?? 0) - b[key];
38
+ for (const key in out)
39
+ if (out[key] === 0)
40
+ delete out[key];
41
+ return out;
42
+ };
23
43
  export function show(x) {
24
44
  const parts = Object.entries(x).filter(([key]) => key !== '1').map(([key, c]) => (c === 1 ? key : c === -1 ? `-${key}` : `${c}${key}`));
25
45
  if ((x['1'] ?? 0) !== 0 || parts.length === 0)
@@ -180,3 +200,94 @@ function prune(steps, last) {
180
200
  return s.kind === 'combine' ? { ...s, a: at.get(s.a), b: at.get(s.b) } : s.kind === 'cut' ? { ...s, of: at.get(s.of) } : s;
181
201
  });
182
202
  }
203
+ // A refutation found fast, where there is one over the rationals: Farkas' lemma says rows a·x + c <= 0 (x free) have no
204
+ // rational solution exactly when some λ >= 0 has Σ λ·a = 0 and Σ λ·c > 0. A floating-point simplex looks for such λ
205
+ // (phase one on Σ λ·a = 0, Σ λ·c = 1), and elimination on the rows it uses alone (usually a handful) gives the
206
+ // derivation, renumbered to the whole system's rows. Floating point only picks the rows: the derivation is exact, and
207
+ // the checker sees nothing else. Undefined when no λ turns up (a system with a rational solution, or one the floats
208
+ // misjudge): the caller goes on as without it.
209
+ export function refuteFast(constraints, maxRows = 2_000) {
210
+ const m = constraints.length;
211
+ if (m === 0)
212
+ return undefined;
213
+ const names = [...new Set(constraints.flatMap(c => vars(c.lin)))], n = names.length;
214
+ if (m > maxRows || n > maxRows)
215
+ return undefined;
216
+ // Tableau rows: one per variable (Σ λ_i a_ij = 0) and the normalization (Σ λ_i c_i = 1); columns: the m λs, then one
217
+ // artificial per row, then the right-hand side.
218
+ const R = n + 1, cols = m + R;
219
+ const T = Array.from({ length: R }, () => new Float64Array(cols + 1));
220
+ const col = new Map(names.map((x, j) => [x, j]));
221
+ constraints.forEach((c, i) => {
222
+ for (const [key, a] of Object.entries(c.lin)) {
223
+ if (key === '1')
224
+ T[n][i] = a;
225
+ else
226
+ T[col.get(key)][i] = a;
227
+ }
228
+ });
229
+ T[n][cols] = 1;
230
+ for (let r = 0; r < R; r++)
231
+ T[r][m + r] = 1;
232
+ const basis = Array.from({ length: R }, (_, r) => m + r);
233
+ // Phase one: minimize the artificials' sum, as the reduced costs of the objective row.
234
+ const z = new Float64Array(cols + 1);
235
+ for (let r = 0; r < R; r++)
236
+ for (let j = 0; j <= cols; j++)
237
+ if (j < m || j === cols)
238
+ z[j] -= T[r][j];
239
+ const eps = 1e-9;
240
+ for (let iter = 0; iter < 50 * (R + m); iter++) {
241
+ // Bland's rule: the first column with a negative reduced cost enters (no cycling on degenerate systems).
242
+ let enter = -1;
243
+ for (let j = 0; j < cols; j++)
244
+ if (z[j] < -eps) {
245
+ enter = j;
246
+ break;
247
+ }
248
+ if (enter < 0)
249
+ break;
250
+ let leave = -1, best = Infinity;
251
+ for (let r = 0; r < R; r++) {
252
+ const a = T[r][enter];
253
+ if (a > eps) {
254
+ const ratio = T[r][cols] / a;
255
+ if (ratio < best - eps || (Math.abs(ratio - best) <= eps && basis[r] < basis[leave])) {
256
+ best = ratio;
257
+ leave = r;
258
+ }
259
+ }
260
+ }
261
+ if (leave < 0)
262
+ return undefined; // unbounded: cannot happen in phase one; give up rather than guess
263
+ const p = T[leave], pv = p[enter];
264
+ for (let j = 0; j <= cols; j++)
265
+ p[j] /= pv;
266
+ for (let r = 0; r < R; r++)
267
+ if (r !== leave) {
268
+ const f = T[r][enter];
269
+ if (f !== 0) {
270
+ const row = T[r];
271
+ for (let j = 0; j <= cols; j++)
272
+ row[j] -= f * p[j];
273
+ }
274
+ }
275
+ const f = z[enter];
276
+ if (f !== 0)
277
+ for (let j = 0; j <= cols; j++)
278
+ z[j] -= f * p[j];
279
+ basis[leave] = enter;
280
+ }
281
+ if (-z[cols] > 1e-7)
282
+ return undefined; // the artificials can't all leave: no λ, so a rational solution exists
283
+ const support = [];
284
+ for (let r = 0; r < R; r++)
285
+ if (basis[r] < m && T[r][cols] > eps)
286
+ support.push(basis[r]);
287
+ if (!support.length)
288
+ return undefined;
289
+ const sub = solve(support.sort((a, b) => a - b).map(i => constraints[i]));
290
+ if (sub.kind !== 'refuted')
291
+ return undefined;
292
+ return { kind: 'refuted', steps: sub.steps.map(st => (st.kind === 'input' ? { ...st, index: support[st.index] } : st)) };
293
+ }
@@ -18,6 +18,7 @@ export type Path = Map<string, {
18
18
  holds: boolean;
19
19
  }>;
20
20
  export declare const canon: (lin: Lin) => string;
21
+ export declare const canonFlip: (lin: Lin) => string;
21
22
  export declare const atomOf: (lin: Lin, holds: boolean) => Constraint;
22
23
  export declare function linear(e: E, path: Path): Lin | undefined;
23
24
  export declare const termColumn: (e: E) => string;
@@ -27,6 +28,12 @@ export declare function termRows(name: string, path: Path): Lin[];
27
28
  export declare function truth(c: C, path: Path): boolean | undefined;
28
29
  export declare function facts(c: C, path: Path, positive?: boolean): Lin[];
29
30
  export declare function leafConstraints(goal: Goal, path: Path): Constraint[];
31
+ export declare function leafConstraintsReusing(goal: Goal, path: Path): Constraint[];
32
+ type Seen<T> = {
33
+ reads: [string, boolean | undefined][];
34
+ out: T;
35
+ }[];
36
+ export declare function reusing<T>(seen: Seen<T>, path: Path, f: (path: Path) => T): T;
30
37
  export declare function withProducts(rows: Constraint[], pairs: [number, number][]): Constraint[];
31
38
  export declare function check(goal: Goal, tree: Tree, path?: Path): boolean;
32
39
  export type Subst = {
@@ -46,7 +53,7 @@ export declare function historyMachine(m: Machine): {
46
53
  machine: Machine;
47
54
  goals: Goal[];
48
55
  };
49
- export declare function obligations(m: Machine, laws: Law[], instances?: Record<string, Instances>): Goal[];
56
+ export declare function obligations(m: Machine, laws: Law[], instances?: Record<string, Instances>, only?: number): Goal[];
50
57
  export declare function checkLaws(m: Machine, laws: Law[], proof: {
51
58
  instances: Record<string, Instances>;
52
59
  trees: Record<string, Tree>;
@@ -122,3 +129,4 @@ export type Fold = {
122
129
  assume?: C;
123
130
  };
124
131
  export declare function foldObligations(f: Fold, laws: Law[]): Goal[];
132
+ export {};
@@ -22,25 +22,99 @@ import { add, k, scale, sub, v } from "../core/solver.js";
22
22
  // An exhaustive switch's default: TypeScript proves it unreachable, and if it is reached anyway (a value from outside
23
23
  // the types), it fails loudly instead of returning undefined.
24
24
  export function unreachable(value) { throw new Error(`unhandled case: ${JSON.stringify(value)}`); }
25
- export const canon = (lin) => Object.keys(lin).sort().map(key => `${key}:${lin[key]}`).join(',');
25
+ // (Remembered per linear form: a form is never changed once built, and the same forms are looked up at every node.)
26
+ const canonMemo = new WeakMap(), flipMemo = new WeakMap();
27
+ export const canon = (lin) => {
28
+ let out = canonMemo.get(lin);
29
+ if (out === undefined)
30
+ canonMemo.set(lin, (out = Object.keys(lin).sort().map(key => `${key}:${lin[key]}`).join(',')));
31
+ return out;
32
+ };
33
+ // canon(1 − lin), the atom that decides lin <= 0 the other way, without building it.
34
+ export const canonFlip = (lin) => {
35
+ let out = flipMemo.get(lin);
36
+ if (out !== undefined)
37
+ return out;
38
+ const keys = Object.keys(lin);
39
+ if (!('1' in lin))
40
+ keys.push('1');
41
+ const parts = [];
42
+ for (const key of keys.sort()) {
43
+ const c = key === '1' ? 1 - (lin['1'] ?? 0) : -lin[key];
44
+ if (c !== 0)
45
+ parts.push(`${key}:${c}`);
46
+ }
47
+ flipMemo.set(lin, (out = parts.join(',')));
48
+ return out;
49
+ };
26
50
  export const atomOf = (lin, holds) => ({ lin: holds ? lin : add(k(1), scale(lin, -1)), label: '' });
27
51
  // Is lin <= 0 decided by the path? A constant atom decides itself.
28
52
  // Over the integers lin <= 0 fails exactly when 1 - lin <= 0 holds, so either atom decides the other.
29
53
  function decide(lin, path) {
30
- if (Object.keys(lin).every(key => key === '1'))
54
+ let constant = true;
55
+ for (const key in lin)
56
+ if (key !== '1') {
57
+ constant = false;
58
+ break;
59
+ }
60
+ if (constant)
31
61
  return (lin['1'] ?? 0) <= 0;
32
- const direct = path.get(canon(lin))?.holds;
62
+ const direct = look(path, canon(lin));
33
63
  if (direct !== undefined)
34
64
  return direct;
35
- const flipped = path.get(canon(add(k(1), scale(lin, -1))))?.holds;
65
+ const flipped = look(path, canonFlip(lin));
36
66
  return flipped === undefined ? undefined : !flipped;
37
67
  }
68
+ // Every decision a hypothesis' facts read, found or not, while they are recorded (leafConstraintsReusing).
69
+ let recording;
70
+ const look = (path, key) => { const out = path.get(key)?.holds; if (recording)
71
+ recording.set(key, out); return out; };
38
72
  // A term's linear form under the path. Reads become cells, named by map and key: `M[key]`; a choice the path leaves
39
73
  // open (∸, min, max, if) becomes a column of its own, `⟦n⟧`, standing for the term's value (see termRows).
40
74
  // Remembered per path and term (a path is never changed once built; a new decision is a new path), so a term shared
41
75
  // across hypotheses, or nested in a k-step run's later states, is read once per path.
76
+ // A term without a choice (no ∸, min, max or if inside, its keys' too) reads alike on every path: remembered once.
42
77
  const linearMemo = new WeakMap();
78
+ const freeMemo = new WeakMap(), freeLinear = new WeakMap();
79
+ const choiceFree = (e) => {
80
+ let out = freeMemo.get(e);
81
+ if (out !== undefined)
82
+ return out;
83
+ switch (e.op) {
84
+ case 'var':
85
+ case 'const':
86
+ out = true;
87
+ break;
88
+ case 'mul':
89
+ out = choiceFree(e.a);
90
+ break;
91
+ case 'get':
92
+ out = choiceFree(e.key);
93
+ break;
94
+ case 'add':
95
+ case 'prod':
96
+ out = choiceFree(e.a) && choiceFree(e.b);
97
+ break;
98
+ case 'sub':
99
+ case 'min':
100
+ case 'max':
101
+ case 'if':
102
+ out = false;
103
+ break;
104
+ }
105
+ freeMemo.set(e, out);
106
+ return out;
107
+ };
43
108
  export function linear(e, path) {
109
+ if (choiceFree(e)) {
110
+ if (freeLinear.has(e))
111
+ return freeLinear.get(e);
112
+ const out = linearOf(e, path);
113
+ freeLinear.set(e, out);
114
+ return out;
115
+ }
116
+ if (reuse)
117
+ return reusing(seenOf(linearSeen, e), path, p => linearOf(e, p));
44
118
  let memo = linearMemo.get(path);
45
119
  if (!memo)
46
120
  linearMemo.set(path, (memo = new WeakMap()));
@@ -158,8 +232,28 @@ function times(a, b) {
158
232
  }
159
233
  return add(...out);
160
234
  }
161
- // The atoms (lin <= 0 each) that make a comparison true, and those that make it false (one of which holds).
235
+ const comparisonMemo = new WeakMap(), freeComparison = new WeakMap();
162
236
  function comparison(c, path) {
237
+ const free = choiceFree(c.a) && choiceFree(c.b);
238
+ if (free && freeComparison.has(c))
239
+ return freeComparison.get(c);
240
+ if (!free && reuse)
241
+ return reusing(seenOf(comparisonSeen, c), path, p => comparisonOf(c, p));
242
+ let memo = comparisonMemo.get(path);
243
+ if (!free) {
244
+ if (!memo)
245
+ comparisonMemo.set(path, (memo = new WeakMap()));
246
+ if (memo.has(c))
247
+ return memo.get(c);
248
+ }
249
+ const out = comparisonOf(c, path);
250
+ if (free)
251
+ freeComparison.set(c, out);
252
+ else
253
+ memo.set(c, out);
254
+ return out;
255
+ }
256
+ function comparisonOf(c, path) {
163
257
  const a = linear(c.a, path), b = linear(c.b, path);
164
258
  if (!a || !b)
165
259
  return undefined;
@@ -170,8 +264,21 @@ function comparison(c, path) {
170
264
  return { when: [add(ab, k(1))], unless: [[ba]] };
171
265
  return { when: [ab, ba], unless: [[add(ab, k(1))], [add(ba, k(1))]] };
172
266
  }
173
- // Kleene truth under the path: true, false, or undefined when open.
267
+ // Kleene truth under the path: true, false, or undefined when open (remembered per path and condition).
268
+ const truthMemo = new WeakMap();
174
269
  export function truth(c, path) {
270
+ if (reuse)
271
+ return reusing(seenOf(truthSeen, c), path, p => truthOf(c, p));
272
+ let memo = truthMemo.get(path);
273
+ if (!memo)
274
+ truthMemo.set(path, (memo = new WeakMap()));
275
+ if (memo.has(c))
276
+ return memo.get(c);
277
+ const out = truthOf(c, path);
278
+ memo.set(c, out);
279
+ return out;
280
+ }
281
+ function truthOf(c, path) {
175
282
  switch (c.op) {
176
283
  case 'flag': throw new Error('flags are inlined before the kernel sees them');
177
284
  case 'all': return undefined; // closed before a goal is checked; opaque here, so it gives a leaf nothing
@@ -224,10 +331,57 @@ export function facts(c, path, positive = true) {
224
331
  }
225
332
  }
226
333
  // What a leaf's derivation must refute.
227
- export function leafConstraints(goal, path) {
334
+ export function leafConstraints(goal, path) { return leafRows(goal, path, h => facts(h, path)); }
335
+ const factsSeen = new WeakMap();
336
+ export function leafConstraintsReusing(goal, path) {
337
+ let byHyp = factsSeen.get(goal);
338
+ if (!byHyp)
339
+ factsSeen.set(goal, (byHyp = new Map()));
340
+ return leafRows(goal, path, h => {
341
+ let seen = byHyp.get(h);
342
+ if (!seen)
343
+ byHyp.set(h, (seen = []));
344
+ return reusing(seen, path, p => facts(h, p));
345
+ });
346
+ }
347
+ export function reusing(seen, path, f) {
348
+ const hit = seen.find(e => e.reads.every(([key, holds]) => path.get(key)?.holds === holds));
349
+ if (hit) {
350
+ if (recording)
351
+ for (const [key, holds] of hit.reads)
352
+ recording.set(key, holds);
353
+ return hit.out;
354
+ }
355
+ const outer = recording, mode = reuse;
356
+ recording = new Map();
357
+ reuse = true;
358
+ try {
359
+ const out = f(new Map(path));
360
+ seen.unshift({ reads: [...recording], out });
361
+ if (seen.length > 16)
362
+ seen.pop();
363
+ return out;
364
+ }
365
+ finally {
366
+ // What was read inside is read by the outer value too.
367
+ if (outer)
368
+ for (const [key, holds] of recording)
369
+ outer.set(key, holds);
370
+ recording = outer;
371
+ reuse = mode;
372
+ }
373
+ }
374
+ // While the prover reuses (inside `reusing`), a term's linear form, a comparison and a condition's truth are reused the
375
+ // same way, each by the decisions it reads: only what a new decision touches is read again. The kernel's check never
376
+ // runs in this mode: it reads every term afresh on every path.
377
+ let reuse = false;
378
+ const linearSeen = new WeakMap(), comparisonSeen = new WeakMap(), truthSeen = new WeakMap();
379
+ const seenOf = (m, k) => { let s = m.get(k); if (!s)
380
+ m.set(k, (s = [])); return s; };
381
+ function leafRows(goal, path, factsOf) {
228
382
  const rows = [...path.values()].map(a => atomOf(a.lin, a.holds));
229
383
  for (const h of goal.hyps)
230
- for (const lin of facts(h, path))
384
+ for (const lin of factsOf(h))
231
385
  rows.push({ lin, label: '' });
232
386
  const names = new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key !== '1')));
233
387
  // Two reads of one map whose keys the path proves equal are equal.
@@ -755,10 +909,11 @@ export function historyMachine(m) {
755
909
  }
756
910
  return { machine, goals };
757
911
  }
758
- export function obligations(m, laws, instances = {}) {
912
+ // `only`: the goals of that law alone (the prover's instance search asks for one law's at a time).
913
+ export function obligations(m, laws, instances = {}, only) {
759
914
  if (m.history) {
760
915
  const { machine, goals } = historyMachine(m);
761
- return [...obligations(machine, laws, instances), ...goals];
916
+ return [...obligations(machine, laws, instances, only), ...(only === undefined ? goals : [])];
762
917
  }
763
918
  scope(m, laws);
764
919
  const ok = [...Object.values(m.state), ...Object.values(m.step.state ?? {}), ...Object.values(m.step.maps ?? {}).flatMap(w => ('each' in w ? [w.value] : [w.key, w.value]))].every(natural)
@@ -770,7 +925,9 @@ export function obligations(m, laws, instances = {}) {
770
925
  // value of it too (the Lean twin reads it so).
771
926
  const proven = (law) => (law.forall ? substC(law.holds, { vars: { [law.forall]: { op: 'var', name: `${law.forall}#` } }, maps: {}, flags: {} }) : law.holds);
772
927
  const pre = preOf(m), u = updateOf(m), assume = m.assume ? [inlineFlags(m, m.assume)] : [];
773
- return laws.flatMap(law => {
928
+ return laws.flatMap((law, i) => {
929
+ if (only !== undefined && i !== only)
930
+ return [];
774
931
  // At the start: the precondition, and no request. One step: from a state where the laws hold, a request allowed.
775
932
  // The facts hold throughout, read where the run begins (they read no state).
776
933
  const start = given(m, holds(from(startOf(m)), pre), instances[law.name] ?? {});
@@ -885,8 +1042,9 @@ export function stepLawObligations(machine, laws, instances, sl) {
885
1042
  const m = machine.history ? historyMachine(machine).machine : machine;
886
1043
  scope(m, laws);
887
1044
  const k = sl.steps ?? 1;
888
- if (!Number.isInteger(k) || k < 1 || k > 16)
889
- throw new Error(`step law ${sl.name}: steps is a whole number from 1 to 16`);
1045
+ // Past 64 the cost is the bound (proving grows about as k³: 64 steps in about 90 s; Lean checks 32 in about 13 minutes).
1046
+ if (!Number.isInteger(k) || k < 1 || k > 64)
1047
+ throw new Error(`step law ${sl.name}: steps is a whole number from 1 to 64`);
890
1048
  const whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when];
891
1049
  if (whens.length > k)
892
1050
  throw new Error(`step law ${sl.name}: one when per step at most`);
@@ -1,5 +1,26 @@
1
1
  import type { C, E, Law, Machine } from '../core/machine.js';
2
2
  import { type Goal, type Instances, type Liveness, type Path, type StepLaw, type Tree } from './kernel.js';
3
+ export declare const profile: {
4
+ on: boolean;
5
+ goals: {
6
+ name: string;
7
+ ms: number;
8
+ nodes: number;
9
+ solves: number;
10
+ solveMs: number;
11
+ rowsMs: number;
12
+ proven: boolean;
13
+ }[];
14
+ solves: number;
15
+ solveMs: number;
16
+ rowsMs: number;
17
+ nodes: number;
18
+ };
19
+ export declare const solveKinds: Record<string, {
20
+ n: number;
21
+ ms: number;
22
+ rows: number;
23
+ }>;
3
24
  export type Outcome = {
4
25
  tree: Tree;
5
26
  nodes: number;