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.
- package/README.md +18 -6
- package/dist/core/dsl.d.ts +106 -8
- package/dist/core/dsl.js +310 -31
- package/dist/core/gdp.js +2 -2
- package/dist/core/machine.d.ts +3 -0
- package/dist/core/machine.js +41 -3
- package/dist/core/solver.d.ts +1 -0
- package/dist/core/solver.js +116 -5
- package/dist/kernel/kernel.d.ts +9 -1
- package/dist/kernel/kernel.js +171 -13
- package/dist/kernel/prove.d.ts +21 -0
- package/dist/kernel/prove.js +128 -22
- package/dist/lean-kernel/emit.js +8 -1
- package/package.json +2 -2
package/dist/core/machine.js
CHANGED
|
@@ -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
|
|
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':
|
|
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) {
|
package/dist/core/solver.d.ts
CHANGED
package/dist/core/solver.js
CHANGED
|
@@ -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
|
|
15
|
-
out[key] = (out[key] ?? 0) +
|
|
16
|
-
for (const key
|
|
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) =>
|
|
22
|
-
|
|
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
|
+
}
|
package/dist/kernel/kernel.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
62
|
+
const direct = look(path, canon(lin));
|
|
33
63
|
if (direct !== undefined)
|
|
34
64
|
return direct;
|
|
35
|
-
const flipped = path
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
889
|
-
|
|
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`);
|
package/dist/kernel/prove.d.ts
CHANGED
|
@@ -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;
|