gdp-wired 0.10.0 → 0.10.1

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.
@@ -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] ?? {});
@@ -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;
@@ -1,5 +1,5 @@
1
- import { add, k, scale, solve, sub, v } from "../core/solver.js";
2
- import { assumed, canon, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
1
+ import { add, k, refuteFast, scale, solve, sub, v } from "../core/solver.js";
2
+ import { assumed, canon, canonFlip, columnTerm, decided, stepZone, check, checkLaws, checkStepLaws, historyMachine, leafConstraints, leafConstraintsReusing, reusing, linear, livenessHelpful, livenessParts, obligations, plainKey, progressObligations, stepLawObligations, trueC, truth, unreachable, withProducts } from "./kernel.js";
3
3
  // The atoms the path leaves open, in evaluation order: deciding any of them makes progress.
4
4
  function openE(e, path, out) {
5
5
  switch (e.op) {
@@ -38,17 +38,18 @@ function openC(c, path, out) {
38
38
  return openC(c.x, path, out);
39
39
  if (c.op === 'and' || c.op === 'or')
40
40
  return c.xs.forEach(y => openC(y, path, out));
41
- openE(c.a, path, out), openE(c.b, path, out);
42
- const a = linear(c.a, path), b = linear(c.b, path);
41
+ const a = linear(c.a, path), b = linear(c.b, path), own = [];
43
42
  if (a && b) {
44
43
  const d = sub(a, b);
45
44
  if (c.op === '<=')
46
- out.push(d);
45
+ own.push(d);
47
46
  else if (c.op === '<')
48
- out.push(add(d, k(1)));
47
+ own.push(add(d, k(1)));
49
48
  else
50
- out.push(d, scale(d, -1));
49
+ own.push(d, scale(d, -1));
51
50
  }
51
+ openE(c.a, path, out), openE(c.b, path, out);
52
+ out.push(...own);
52
53
  }
53
54
  // A hypothesis is settled when it gives facts that are all its content: here, when its truth is decided, or it is a
54
55
  // conjunction of settled parts / a linear comparison. A disjunction is settled only once one disjunct is left open and
@@ -85,7 +86,7 @@ function openCells(rows, path) {
85
86
  if (p.slice(0, p.indexOf('[')) !== q.slice(0, q.indexOf('[')))
86
87
  continue;
87
88
  const d = sub(parse(keyText(p)), parse(keyText(q)));
88
- const dec = (lin) => path.get(canon(lin))?.holds ?? (path.has(canon(add(k(1), scale(lin, -1)))) ? !path.get(canon(add(k(1), scale(lin, -1)))).holds : undefined);
89
+ const dec = (lin) => path.get(canon(lin))?.holds ?? (path.has(canonFlip(lin)) ? !path.get(canonFlip(lin)).holds : undefined);
89
90
  const a = dec(d), b = dec(scale(d, -1));
90
91
  if (a === undefined)
91
92
  out.push(d);
@@ -113,9 +114,40 @@ function parse(text) {
113
114
  }
114
115
  return out;
115
116
  }
117
+ // Where a goal's time goes, when asked (research/perf/prover-bench.ts): each root goal's time, nodes, solver calls and
118
+ // the time in the solver and in building rows. Off unless `profile.on`; the counters cost nothing then.
119
+ export const profile = { on: false, goals: [], solves: 0, solveMs: 0, rowsMs: 0, nodes: 0 };
120
+ const timed = (f, add) => { if (!profile.on)
121
+ return f(); const t = performance.now(); try {
122
+ return f();
123
+ }
124
+ finally {
125
+ add(performance.now() - t);
126
+ } };
127
+ export const solveKinds = {};
128
+ const solveP = (rows, maxRows) => {
129
+ if (!profile.on)
130
+ return solve(rows, maxRows);
131
+ const t = performance.now(), out = solve(rows, maxRows), ms = performance.now() - t;
132
+ profile.solves++;
133
+ profile.solveMs += ms;
134
+ const s = (solveKinds[out.kind] ??= { n: 0, ms: 0, rows: 0 });
135
+ s.n++;
136
+ s.ms += ms;
137
+ s.rows += rows.length;
138
+ return out;
139
+ };
116
140
  // The budget: 200,000 nodes and, by default, a second per hypothesis (at least 20 seconds) per goal; past either the goal
117
141
  // is left `unknown`. A goal with many instances (a law about two keys at its pairs) has that many more rows per leaf.
118
142
  export function grow(goal, path = new Map(), budget = { nodes: 0, until: performance.now() + Math.max(20_000, 1_000 * goal.hyps.length) }) {
143
+ if (!profile.on || path.size || budget.nodes)
144
+ return growRoot(goal, path, budget);
145
+ const t = performance.now(), s0 = profile.solves, m0 = profile.solveMs, r0 = profile.rowsMs, n0 = profile.nodes;
146
+ const out = growRoot(goal, path, budget);
147
+ profile.goals.push({ name: goal.name, ms: performance.now() - t, nodes: profile.nodes - n0, solves: profile.solves - s0, solveMs: profile.solveMs - m0, rowsMs: profile.rowsMs - r0, proven: 'tree' in out });
148
+ return out;
149
+ }
150
+ function growRoot(goal, path, budget) {
119
151
  // At the root: plainly first; where the cases multiply past a budget, through cuts. The plain budget halves with each
120
152
  // two open choices (what a plain search's cases grow by), so a long run's steps reach the cuts early.
121
153
  if (path.size === 0 && budget.nodes === 0) {
@@ -132,24 +164,47 @@ export function grow(goal, path = new Map(), budget = { nodes: 0, until: perform
132
164
  function growPlain(goal, path, budget) {
133
165
  if (budget.cap !== undefined && budget.nodes >= budget.cap)
134
166
  return { stuck: 'unknown', path, why: 'cap' };
167
+ if (profile.on)
168
+ profile.nodes++;
135
169
  if (++budget.nodes > 200_000 || (budget.until !== undefined && performance.now() > budget.until))
136
170
  return { stuck: 'unknown', path, why: outOfBudget(goal, budget.nodes > 200_000 ? 'nodes' : 'time', path.size) };
137
171
  // Rows that read a term column only once a cut has put one on the path; before, the rows a choice-free search uses
138
172
  // (what it saw before term columns), renumbered to the whole leaf's when a leaf is found.
139
- const full = leafConstraints(goal, path);
173
+ const full = timed(() => leafConstraintsReusing(goal, path), ms => { profile.rowsMs += ms; });
140
174
  const columns = [...path.values()].some(a => Object.keys(a.lin).some(key => key.startsWith('⟦')));
141
175
  const idx = full.flatMap((r, i) => (columns || !Object.keys(r.lin).some(key => key.startsWith('⟦')) ? [i] : []));
142
176
  const rows = idx.map(i => full[i]), at = (i) => (i < rows.length ? idx[i] : full.length + i - rows.length);
143
177
  const leafOf = (steps, products) => ({ leaf: steps.map(st => (st.kind === 'input' ? { ...st, index: at(st.index) } : st)), ...(products ? { products: products.map(([i, j]) => [at(i), at(j)]) } : {}) });
144
- const verdict = solve(rows);
145
- if (verdict.kind === 'refuted')
146
- return { tree: leafOf(verdict.steps), nodes: budget.nodes };
178
+ // The conclusion's own atoms first (the goal's last hypothesis is the law refuted): a case it doesn't read is often
179
+ // one no refutation needs, and splitting it first multiplies every case after it.
147
180
  const open = [];
148
- for (const h of goal.hyps)
149
- openH(h, path, open);
181
+ const hyps = goal.hyps, last = hyps.length - 1;
182
+ let byHyp = openSeen.get(goal);
183
+ if (!byHyp)
184
+ openSeen.set(goal, (byHyp = new Map()));
185
+ const openOf = (h) => { let seen = byHyp.get(h); if (!seen)
186
+ byHyp.set(h, (seen = [])); return reusing(seen, path, p => { const out = []; openH(h, p, out); return out; }); };
187
+ if (last >= 0)
188
+ open.push(...openOf(hyps[last]));
189
+ for (const [i, h] of hyps.entries())
190
+ if (i !== last)
191
+ open.push(...openOf(h));
150
192
  open.push(...openCells(rows, path));
151
- let split = open.find(lin => Object.keys(lin).some(key => key !== '1') && !path.has(canon(lin)) && !path.has(canon(add(k(1), scale(lin, -1)))))
152
- ?? (verdict.kind === 'unknown' ? bisect(rows, path) ?? fractional(verdict.point, path) : undefined);
193
+ const next = open.find(lin => Object.keys(lin).some(key => key !== '1') && !path.has(canon(lin)) && !path.has(canonFlip(lin)));
194
+ // With a case still to split, only "refuted or not" matters (a witness here goes unused): a large system is tried by
195
+ // its Farkas rows alone, and left to the split if they don't refute it. Elimination to the last variable, which a
196
+ // satisfiable system needs to say so, is kept for small systems and for the leaves, where its answer is the outcome.
197
+ // Past the Farkas rows, elimination still runs, bounded (a refutation the rationals miss is an integer one, which
198
+ // elimination's cuts find early; a satisfiable system grows past the bound and is left to the split).
199
+ if (next && rows.length > 60) {
200
+ const fast = timed(() => refuteFast(rows), ms => { profile.solves++; profile.solveMs += ms; });
201
+ if (fast?.kind === 'refuted')
202
+ return { tree: leafOf(fast.steps), nodes: budget.nodes };
203
+ }
204
+ const verdict = next && rows.length > 60 ? solveP(rows, 2 * rows.length) : solveP(rows);
205
+ if (verdict.kind === 'refuted')
206
+ return { tree: leafOf(verdict.steps), nodes: budget.nodes };
207
+ let split = next ?? (verdict.kind === 'unknown' ? bisect(rows, path) ?? fractional(verdict.point, path) : undefined);
153
208
  // Products: with monomials about, try every pair of rows multiplied; failing that, split a factor at zero.
154
209
  const monomials = rows.some(r => Object.keys(r.lin).some(key => key.includes('*')));
155
210
  // A witness that multiplies out (each monomial's value is its factors' product) is a real model: nothing to refute.
@@ -184,7 +239,7 @@ function growPlain(goal, path, budget) {
184
239
  return { tree: leafOf(third.steps, [...sq, ...cubes]), nodes: budget.nodes };
185
240
  }
186
241
  const factors = [...new Set(rows.flatMap(r => Object.keys(r.lin).filter(key => key.includes('*')).flatMap(key => key.split('*'))))];
187
- const fresh = (lin) => !path.has(canon(lin)) && !path.has(canon(add(k(1), scale(lin, -1))));
242
+ const fresh = (lin) => !path.has(canon(lin)) && !path.has(canonFlip(lin));
188
243
  // Split a factor at zero, else two factors against each other (so (x - y)² >= 0 becomes a product of rows).
189
244
  const zero = factors.find(f => fresh({ [f]: 1 }));
190
245
  const apart = factors.flatMap((f, i) => factors.slice(i + 1).map(g => ({ [f]: 1, [g]: -1 }))).find(fresh);
@@ -199,11 +254,62 @@ function growPlain(goal, path, budget) {
199
254
  const yes = growPlain(goal, new Map(path).set(id, { lin: split, holds: true }), budget);
200
255
  if (!('tree' in yes))
201
256
  return yes;
257
+ // A refutation of the yes side that never reads the split refutes the case as it stands: the other side is skipped.
258
+ const lifted = lift(goal, yes.tree, new Map(path).set(id, { lin: split, holds: true }), path);
259
+ if (lifted)
260
+ return { tree: lifted, nodes: budget.nodes };
202
261
  const no = growPlain(goal, new Map(path).set(id, { lin: split, holds: false }), budget);
203
262
  if (!('tree' in no))
204
263
  return no;
205
264
  return { tree: { split, yes: yes.tree, no: no.tree }, nodes: budget.nodes };
206
265
  }
266
+ // A tree found under one more decision (`from`, the path with it) as a tree under the path without it (`to`), if no
267
+ // leaf reads the decision: each leaf's rows found again among the rows the shorter path gives (the same rows, renumbered:
268
+ // the decision's own row, and any row only it makes, are not there), every split kept. The derivations are the same
269
+ // arithmetic over the same rows, and the kernel checks the whole tree in the end. Undefined at the first leaf that
270
+ // needs the decision.
271
+ function lift(goal, tree, from, to) {
272
+ const own = to.size; // the decision's row: the last of `from`'s path rows, below every later split's
273
+ const reads = (t) => ('leaf' in t ? t.leaf.some(st => st.kind === 'input' && st.index === own) || (t.products ?? []).some(([i, j]) => i === own || j === own) : reads(t.yes) || reads(t.no));
274
+ if (reads(tree))
275
+ return undefined;
276
+ const go = (t, a, b) => {
277
+ if (!('leaf' in t)) {
278
+ const sid = canon(t.split);
279
+ if (b.has(sid) || b.has(canonFlip(t.split)))
280
+ return undefined;
281
+ const yes = go(t.yes, new Map(a).set(sid, { lin: t.split, holds: true }), new Map(b).set(sid, { lin: t.split, holds: true }));
282
+ const no = yes && go(t.no, new Map(a).set(sid, { lin: t.split, holds: false }), new Map(b).set(sid, { lin: t.split, holds: false }));
283
+ return yes && no ? { split: t.split, yes, no } : undefined;
284
+ }
285
+ const ra = leafConstraintsReusing(goal, a), rb = leafConstraintsReusing(goal, b), at = new Map();
286
+ rb.forEach((r, i) => { const key = canon(r.lin); if (!at.has(key))
287
+ at.set(key, i); });
288
+ const map = (i) => (i < ra.length ? at.get(canon(ra[i].lin)) : undefined);
289
+ const leaf = [];
290
+ for (const st of t.leaf) {
291
+ if (st.kind !== 'input') {
292
+ leaf.push(st);
293
+ continue;
294
+ }
295
+ const j = map(st.index);
296
+ if (j === undefined)
297
+ return undefined;
298
+ leaf.push({ ...st, index: j });
299
+ }
300
+ const products = [];
301
+ for (const [i, j] of t.products ?? []) {
302
+ const pi = map(i), pj = map(j);
303
+ if (pi === undefined || pj === undefined)
304
+ return undefined;
305
+ products.push([pi, pj]);
306
+ }
307
+ return { leaf, ...(t.products ? { products } : {}) };
308
+ };
309
+ return go(tree, from, to);
310
+ }
311
+ // Each hypothesis' open atoms, as at an earlier path that decided the same atoms it reads (kernel.ts's reusing).
312
+ const openSeen = new WeakMap();
207
313
  // What ran out, for a goal left unknown: its size, how deep the case splits went, and the two shapes that usually cause it.
208
314
  function outOfBudget(goal, limit, depth) {
209
315
  const steps = Number(/@(\d+) steps/.exec(goal.name)?.[1] ?? 1);
@@ -259,7 +365,7 @@ function topAtoms(c, path) {
259
365
  if (!a || !b)
260
366
  return [];
261
367
  const atoms = c.op === '<=' ? [sub(a, b)] : c.op === '<' ? [add(sub(a, b), k(1))] : [sub(a, b), sub(b, a)];
262
- return atoms.filter(l => Object.keys(l).some(key => key !== '1') && !path.has(canon(l)) && !path.has(canon(add(k(1), scale(l, -1)))));
368
+ return atoms.filter(l => Object.keys(l).some(key => key !== '1') && !path.has(canon(l)) && !path.has(canonFlip(l)));
263
369
  }
264
370
  // A refutation that splits only on these atoms (a cut's failing side), within a small budget.
265
371
  function growFocused(goal, path, focus, budget, near = focus) {
@@ -268,7 +374,7 @@ function growFocused(goal, path, focus, budget, near = focus) {
268
374
  const verdict = solveNear(leafConstraints(goal, path), [...near, ...[...path.values()].slice(-1).map(a => a.lin)]);
269
375
  if (verdict.kind === 'refuted')
270
376
  return { tree: { leaf: verdict.steps }, nodes: budget.nodes };
271
- const split = focus.find(l => !path.has(canon(l)) && !path.has(canon(add(k(1), scale(l, -1)))));
377
+ const split = focus.find(l => !path.has(canon(l)) && !path.has(canonFlip(l)));
272
378
  if (!split)
273
379
  return { stuck: verdict.kind === 'sat' ? 'sat' : 'unknown', path };
274
380
  const id = canon(split);
@@ -319,7 +425,7 @@ function bisect(rows, path) {
319
425
  const mid = Math.floor((lo + hi) / 2);
320
426
  for (const c of lo === hi ? [lo, lo - 1] : [mid]) {
321
427
  const lin = c === 0 ? { [x]: 1 } : { [x]: 1, 1: -c };
322
- if (!path.has(canon(lin)) && !path.has(canon(add(k(1), scale(lin, -1)))))
428
+ if (!path.has(canon(lin)) && !path.has(canonFlip(lin)))
323
429
  return lin;
324
430
  }
325
431
  }
@@ -331,7 +437,7 @@ function fractional(point, path) {
331
437
  if (Math.abs(value - Math.round(value)) < 1e-9 || x.includes('*'))
332
438
  continue;
333
439
  const c = Math.floor(value), lin = c === 0 ? { [x]: 1 } : { [x]: 1, 1: -c };
334
- if (!path.has(canon(lin)) && !path.has(canon(add(k(1), scale(lin, -1)))))
440
+ if (!path.has(canon(lin)) && !path.has(canonFlip(lin)))
335
441
  return lin;
336
442
  }
337
443
  return undefined;
@@ -616,7 +722,7 @@ function proveLawsAsGiven(m, laws) {
616
722
  // Each law's two goals (start and step) get their own instances, seeded with its own keys.
617
723
  const keys = [...readKeys(m, laws), ...witnesses(m)], instances = {};
618
724
  laws.forEach((law, i) => {
619
- instances[law.name] = instancesFor(m, laws, inst => obligations(m, laws, { ...instances, [law.name]: inst }).slice(2 * i, 2 * i + 2), {
725
+ instances[law.name] = instancesFor(m, laws, inst => obligations(m, laws, { ...instances, [law.name]: inst }, i), {
620
726
  seed: [...keys, ...(law.forall ? [{ op: 'var', name: `${law.forall}#` }] : []), ...(law.keys ?? []).map((name) => ({ op: 'var', name }))],
621
727
  focus: [...(law.forall ? [`${law.forall}#`] : []), ...(law.keys ?? [])],
622
728
  });
@@ -105,12 +105,19 @@ export function encode(ex, tamper = false, dropStep = true, globalVars, globalMa
105
105
  else if (key.includes('*'))
106
106
  monos.add(key);
107
107
  }
108
+ // Each cell a hypothesis reads under the path, kept or not: two reads of one cell cancel in a row (T − T), yet
109
+ // Lean's linear form of each still needs the cell's column to see the comparison decided.
108
110
  for (const h of goal.hyps)
109
- for (const e of subterms(h))
111
+ for (const e of subterms(h)) {
110
112
  if (e.op === 'prod')
111
113
  for (const key of Object.keys(linear(e, path) ?? {}))
112
114
  if (key.includes('*'))
113
115
  monos.add(key);
116
+ if (e.op === 'get')
117
+ for (const key of Object.keys(linear(e, path) ?? {}))
118
+ if (key.includes('['))
119
+ addCell(key);
120
+ }
114
121
  });
115
122
  const depth = (name) => name.split('[').length;
116
123
  // Term columns (open choices, each standing for its term's value) before the cells, whose keys may read them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdp-wired",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "An automatic prover for stateful rules (budgets, caps, windows, workflows, policies): laws proven with no proof to write, checked again in Lean, counterexamples found on the GPU, the rule shipped as code",
5
5
  "keywords": [
6
6
  "verification",