gdp-wired 0.19.0 → 0.21.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 +37 -6
- package/dist/core/cycles.d.ts +35 -2
- package/dist/core/cycles.js +458 -51
- package/dist/core/cyclic.d.ts +18 -4
- package/dist/core/cyclic.js +319 -263
- package/dist/core/data.d.ts +196 -15
- package/dist/core/data.js +1284 -213
- package/dist/core/discover.js +54 -33
- package/dist/core/dsl.d.ts +31 -3
- package/dist/core/dsl.js +168 -38
- package/dist/core/exists.d.ts +26 -0
- package/dist/core/exists.js +625 -0
- package/dist/core/infer.d.ts +13 -0
- package/dist/core/infer.js +258 -41
- package/dist/core/machine.d.ts +27 -0
- package/dist/core/machine.js +45 -0
- package/dist/core/solver.d.ts +3 -0
- package/dist/core/solver.js +5 -0
- package/dist/core/transfer.d.ts +17 -0
- package/dist/core/transfer.js +468 -0
- package/dist/core/translate.js +10 -4
- package/dist/core/watch.js +5 -1
- package/dist/kernel/prove.d.ts +8 -1
- package/dist/kernel/prove.js +106 -55
- package/dist/lean-kernel/emit.d.ts +4 -1
- package/dist/lean-kernel/emit.js +336 -69
- package/dist/lean-kernel/library.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// There exists: a law ∃ x. P(x) (core/machine.ts's Law with `some`: x a natural, or an integer as two, x and x$neg),
|
|
2
|
+
// proven through a witness. Nothing here is trusted: the kernel proves a plain law of a plain machine, and the Lean
|
|
3
|
+
// export states the existential again, proven by ⟨witness, proof⟩ (lean-kernel/Ghost.lean's some_updAll).
|
|
4
|
+
// - a witness term (`terms`): a term of the state for each bound value; the law proven is P with the terms in place,
|
|
5
|
+
// so at every reachable state the term's value is a witness
|
|
6
|
+
// - a ghost (`ghost`): each bound value a state variable the machine gains, with a value at the start (over the
|
|
7
|
+
// params) and after each step (over the state before, the request, the flags and the ghost's own value before);
|
|
8
|
+
// the law proven is P over the ghosts. Read as a hypothesis, the law at the state before a step is P at the ghost's
|
|
9
|
+
// value there: a fresh value for each existential and each step, the one the run carried, while the step's own
|
|
10
|
+
// witness is the ghost's value after (Ghost.lean's sghost_reach: every reachable state of the machine is, outside
|
|
11
|
+
// its ghosts, a reachable state of the machine with them, which no part of it reads)
|
|
12
|
+
// The prover finds the witness by counterexamples (the kernel's models of a goal it doesn't prove): candidates are 0, the
|
|
13
|
+
// params, the state, the values P's own atoms solve for (x = e, x <= e, x < e, as P reads them), and for a ghost its
|
|
14
|
+
// value before, shifts of it, the request's values and the state before; each is chosen to satisfy P at every
|
|
15
|
+
// counterexample so far, per case of the step (the conditions its updates split on: the request's clause, say).
|
|
16
|
+
// A law under a `forall` key takes witness terms only (a ghost per key would be a map).
|
|
17
|
+
import { evalC, evalE } from "./machine.js";
|
|
18
|
+
import { proveLaw, proveLaws, withinDeadline } from "../kernel/prove.js";
|
|
19
|
+
const k = (value) => ({ op: 'const', value });
|
|
20
|
+
const v = (name) => ({ op: 'var', name });
|
|
21
|
+
const add = (a, b) => (a.op === 'const' && a.value === 0 ? b : b.op === 'const' && b.value === 0 ? a : { op: 'add', a, b });
|
|
22
|
+
const sum = (es) => es.reduce((acc, e) => add(acc, e), k(0));
|
|
23
|
+
const sub = (a, b) => (b.op === 'const' && b.value === 0 ? a : { op: 'sub', a, b });
|
|
24
|
+
const key = (x) => JSON.stringify(x);
|
|
25
|
+
export const namesOf = (t) => [...new Set([...JSON.stringify(t).matchAll(/"op":"var","name":"([^"]+)"/g)].map(m => m[1]))];
|
|
26
|
+
const readsMaps = (t) => JSON.stringify(t).includes('"op":"get"');
|
|
27
|
+
// The groups of a law's bound naturals: an integer's x and x$neg together.
|
|
28
|
+
export function groupsOf(vars) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const n of vars)
|
|
31
|
+
if (!n.endsWith('$neg'))
|
|
32
|
+
out.push(vars.includes(`${n}$neg`) ? [n, `${n}$neg`] : [n]);
|
|
33
|
+
for (const n of vars)
|
|
34
|
+
if (n.endsWith('$neg') && !vars.includes(n.slice(0, -4)))
|
|
35
|
+
out.push([n]);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
// Variables replaced by terms, structurally (Lean's substE/substC over variables: Kernel.lean), the rest as they are.
|
|
39
|
+
export function substVarsE(e, s) {
|
|
40
|
+
switch (e.op) {
|
|
41
|
+
case 'var': return s[e.name] ?? e;
|
|
42
|
+
case 'const': return e;
|
|
43
|
+
case 'mul': return { ...e, a: substVarsE(e.a, s) };
|
|
44
|
+
case 'if': return { op: 'if', c: substVars(e.c, s), then: substVarsE(e.then, s), else: substVarsE(e.else, s) };
|
|
45
|
+
case 'get': return { ...e, key: substVarsE(e.key, s) };
|
|
46
|
+
case 'add':
|
|
47
|
+
case 'sub':
|
|
48
|
+
case 'min':
|
|
49
|
+
case 'max':
|
|
50
|
+
case 'prod': return { ...e, a: substVarsE(e.a, s), b: substVarsE(e.b, s) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function substVars(c, s) {
|
|
54
|
+
switch (c.op) {
|
|
55
|
+
case 'flag': return c;
|
|
56
|
+
case 'all': {
|
|
57
|
+
const bound = [c.key, ...(c.keys ?? [])], inner = Object.fromEntries(Object.entries(s).filter(([n]) => !bound.includes(n)));
|
|
58
|
+
return { ...c, c: substVars(c.c, inner) };
|
|
59
|
+
}
|
|
60
|
+
case 'not': return { op: 'not', x: substVars(c.x, s) };
|
|
61
|
+
case 'and':
|
|
62
|
+
case 'or': return { op: c.op, xs: c.xs.map(y => substVars(y, s)) };
|
|
63
|
+
case '<':
|
|
64
|
+
case '<=':
|
|
65
|
+
case '=': return { ...c, a: substVarsE(c.a, s), b: substVarsE(c.b, s) };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function lin(e, bound) {
|
|
69
|
+
const free = !namesOf(e).some(n => bound.has(n));
|
|
70
|
+
switch (e.op) {
|
|
71
|
+
case 'var': return bound.has(e.name) ? { k: { [e.name]: 1 }, pos: [], neg: [] } : { k: {}, pos: [e], neg: [] };
|
|
72
|
+
case 'const': return { k: {}, pos: e.value ? [e] : [], neg: [] };
|
|
73
|
+
case 'add': {
|
|
74
|
+
const a = lin(e.a, bound), b = lin(e.b, bound);
|
|
75
|
+
if (!a || !b)
|
|
76
|
+
return undefined;
|
|
77
|
+
const out = { ...a.k };
|
|
78
|
+
for (const [n, c] of Object.entries(b.k))
|
|
79
|
+
out[n] = (out[n] ?? 0) + c;
|
|
80
|
+
return { k: out, pos: [...a.pos, ...b.pos], neg: [...a.neg, ...b.neg] };
|
|
81
|
+
}
|
|
82
|
+
case 'mul': {
|
|
83
|
+
const a = lin(e.a, bound);
|
|
84
|
+
if (!a)
|
|
85
|
+
return undefined;
|
|
86
|
+
const scale = (es) => es.map(x => (e.k === 1 ? x : { op: 'mul', k: e.k, a: x }));
|
|
87
|
+
return { k: Object.fromEntries(Object.entries(a.k).map(([n, c]) => [n, c * e.k])), pos: scale(a.pos), neg: scale(a.neg) };
|
|
88
|
+
}
|
|
89
|
+
case 'sub':
|
|
90
|
+
case 'min':
|
|
91
|
+
case 'max':
|
|
92
|
+
case 'prod':
|
|
93
|
+
case 'if':
|
|
94
|
+
case 'get': return free ? { k: {}, pos: [e], neg: [] } : undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function atomsOf(c, bound, out = []) {
|
|
98
|
+
switch (c.op) {
|
|
99
|
+
case 'not': return atomsOf(c.x, bound, out);
|
|
100
|
+
case 'and':
|
|
101
|
+
case 'or':
|
|
102
|
+
for (const y of c.xs)
|
|
103
|
+
atomsOf(y, bound, out);
|
|
104
|
+
return out;
|
|
105
|
+
case 'flag':
|
|
106
|
+
case 'all': return out;
|
|
107
|
+
case '<':
|
|
108
|
+
case '<=':
|
|
109
|
+
case '=': {
|
|
110
|
+
const a = lin(c.a, bound), b = lin(c.b, bound);
|
|
111
|
+
if (!a || !b)
|
|
112
|
+
return out;
|
|
113
|
+
const kk = { ...a.k };
|
|
114
|
+
for (const [n, x] of Object.entries(b.k))
|
|
115
|
+
kk[n] = (kk[n] ?? 0) - x;
|
|
116
|
+
for (const n of Object.keys(kk))
|
|
117
|
+
if (kk[n] === 0)
|
|
118
|
+
delete kk[n];
|
|
119
|
+
if (Object.keys(kk).length)
|
|
120
|
+
out.push({ op: c.op, k: kk, plus: [...b.pos, ...a.neg], minus: [...a.pos, ...b.neg] });
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// The conjuncts of c that read a bound value, through and, and an or whose other parts read none (an implication's
|
|
126
|
+
// conclusion: a guard on the state).
|
|
127
|
+
function conjunctsOf(c, bound) {
|
|
128
|
+
const reads = (d) => namesOf(d).some(n => bound.has(n));
|
|
129
|
+
if (c.op === 'and')
|
|
130
|
+
return c.xs.flatMap(d => conjunctsOf(d, bound));
|
|
131
|
+
if (c.op === 'or') {
|
|
132
|
+
const rd = c.xs.filter(reads);
|
|
133
|
+
if (rd.length === 1)
|
|
134
|
+
return conjunctsOf(rd[0], bound);
|
|
135
|
+
}
|
|
136
|
+
return reads(c) ? [c] : [];
|
|
137
|
+
}
|
|
138
|
+
// The values a group's atoms solve for, each with its neighbours: x = R+ − R− (or its negation), x + 1, x − 1.
|
|
139
|
+
function solved(atoms, g) {
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const a of atoms) {
|
|
142
|
+
const others = Object.keys(a.k).filter(n => !g.includes(n));
|
|
143
|
+
if (others.length)
|
|
144
|
+
continue;
|
|
145
|
+
const s = a.k[g[0]] ?? 0;
|
|
146
|
+
if (Math.abs(s) !== 1 || (g.length === 2 && (a.k[g[1]] ?? 0) !== -s) || (g.length === 1 && Object.keys(a.k).length !== 1))
|
|
147
|
+
continue;
|
|
148
|
+
const [hi, lo] = s === 1 ? [sum(a.plus), sum(a.minus)] : [sum(a.minus), sum(a.plus)];
|
|
149
|
+
if (g.length === 2)
|
|
150
|
+
out.push([hi, lo], [add(hi, k(1)), lo], [hi, add(lo, k(1))]);
|
|
151
|
+
else
|
|
152
|
+
out.push([sub(hi, lo)], [add(sub(hi, lo), k(1))], [sub(sub(hi, lo), k(1))]);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
// --- evaluating ∃ on a state ----------------------------------------------------------------------------------------
|
|
157
|
+
// Whether ∃ vars. P holds where env and st give every other value, with a witness: exact when the bound values are one
|
|
158
|
+
// natural or one integer read linearly (P's truth changes only next to the values its atoms solve for, so those, their
|
|
159
|
+
// neighbours, 0 and one past them are every case); otherwise found among those points, or undefined.
|
|
160
|
+
export function someHolds(body, vars, env, st, m) {
|
|
161
|
+
const groups = groupsOf(vars), bound = new Set(vars);
|
|
162
|
+
const value = (e) => { try {
|
|
163
|
+
return evalE(e, env, st, m);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
} };
|
|
168
|
+
const atoms = atomsOf(body, bound);
|
|
169
|
+
const linearOnly = linearIn(body, bound);
|
|
170
|
+
// A bound natural read as a map's key: the cells the maps hold, and one past them, are every case a read gives.
|
|
171
|
+
const cells = new Set();
|
|
172
|
+
if (JSON.stringify(body).includes('"op":"get"'))
|
|
173
|
+
for (const c of Object.values(st.maps))
|
|
174
|
+
for (const key of c.keys())
|
|
175
|
+
cells.add(key);
|
|
176
|
+
const points = (g) => {
|
|
177
|
+
const ps = new Set([0, 1, ...cells, ...[...cells].map(c => c + 1)]);
|
|
178
|
+
for (const a of atoms) {
|
|
179
|
+
const s = a.k[g[0]] ?? 0, s2 = g.length === 2 ? (a.k[g[1]] ?? 0) : 0;
|
|
180
|
+
if (!s || (g.length === 2 && s2 !== -s) || Object.keys(a.k).some(n => !g.includes(n)))
|
|
181
|
+
continue;
|
|
182
|
+
const r = (sumOf(a.plus, value) ?? NaN) - (sumOf(a.minus, value) ?? NaN);
|
|
183
|
+
if (!Number.isFinite(r))
|
|
184
|
+
continue;
|
|
185
|
+
for (const p of [Math.floor(r / s), Math.ceil(r / s)])
|
|
186
|
+
for (const d of [-1, 0, 1])
|
|
187
|
+
ps.add(p + d);
|
|
188
|
+
}
|
|
189
|
+
const xs = [...ps].filter(p => g.length === 2 || p >= 0);
|
|
190
|
+
const hi = Math.max(...xs), lo = Math.min(...xs);
|
|
191
|
+
return [...new Set([...xs, hi + 1, ...(g.length === 2 ? [lo - 1] : [])])].sort((a, b) => a - b);
|
|
192
|
+
};
|
|
193
|
+
const at = (g, p) => (g.length === 2 ? { [g[0]]: Math.max(p, 0), [g[1]]: Math.max(-p, 0) } : { [g[0]]: p });
|
|
194
|
+
const test = (w) => { try {
|
|
195
|
+
return evalC(body, { ...env, ...w }, st, m);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
} };
|
|
200
|
+
const lists = groups.map(points);
|
|
201
|
+
let tried = 0, unknown = false;
|
|
202
|
+
const go = (i, w) => {
|
|
203
|
+
if (i === groups.length) {
|
|
204
|
+
tried++;
|
|
205
|
+
const t = test(w);
|
|
206
|
+
if (t === undefined)
|
|
207
|
+
unknown = true;
|
|
208
|
+
return t ? w : undefined;
|
|
209
|
+
}
|
|
210
|
+
for (const p of lists[i]) {
|
|
211
|
+
if (tried > 4096)
|
|
212
|
+
return undefined;
|
|
213
|
+
const hit = go(i + 1, { ...w, ...at(groups[i], p) });
|
|
214
|
+
if (hit)
|
|
215
|
+
return hit;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
};
|
|
219
|
+
const hit = go(0, {});
|
|
220
|
+
if (hit)
|
|
221
|
+
return { holds: true, witness: hit };
|
|
222
|
+
return groups.length === 1 && linearOnly && !unknown && tried <= 4096 ? { holds: false } : undefined;
|
|
223
|
+
}
|
|
224
|
+
const sumOf = (es, value) => {
|
|
225
|
+
let s = 0;
|
|
226
|
+
for (const e of es) {
|
|
227
|
+
const x = value(e);
|
|
228
|
+
if (x === undefined)
|
|
229
|
+
return undefined;
|
|
230
|
+
s += x;
|
|
231
|
+
}
|
|
232
|
+
return s;
|
|
233
|
+
};
|
|
234
|
+
// Every read of a bound natural is linear (through + and × by a constant only, up to an atom).
|
|
235
|
+
function linearIn(c, bound) {
|
|
236
|
+
switch (c.op) {
|
|
237
|
+
case 'not': return linearIn(c.x, bound);
|
|
238
|
+
case 'and':
|
|
239
|
+
case 'or': return c.xs.every(y => linearIn(y, bound));
|
|
240
|
+
case 'flag': return true;
|
|
241
|
+
case 'all': return !namesOf(c.c).some(n => bound.has(n));
|
|
242
|
+
case '<':
|
|
243
|
+
case '<=':
|
|
244
|
+
case '=': return (lin(c.a, bound) !== undefined && lin(c.b, bound) !== undefined) || (onlyKeys(c.a, bound) && onlyKeys(c.b, bound));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Every read of a bound natural is a map's whole key (M[x]: piecewise constant, its pieces the cells).
|
|
248
|
+
function onlyKeys(e, bound) {
|
|
249
|
+
if (e.op === 'get' && e.key.op === 'var' && bound.has(e.key.name))
|
|
250
|
+
return true;
|
|
251
|
+
if (!namesOf(e).some(n => bound.has(n)))
|
|
252
|
+
return true;
|
|
253
|
+
switch (e.op) {
|
|
254
|
+
case 'var':
|
|
255
|
+
case 'const': return false;
|
|
256
|
+
case 'add':
|
|
257
|
+
case 'sub':
|
|
258
|
+
case 'min':
|
|
259
|
+
case 'max':
|
|
260
|
+
case 'prod': return onlyKeys(e.a, bound) && onlyKeys(e.b, bound);
|
|
261
|
+
case 'mul': return onlyKeys(e.a, bound);
|
|
262
|
+
case 'if': return false;
|
|
263
|
+
case 'get': return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// The conditions a step splits on, in order: the if conditions along each update's else-chain.
|
|
267
|
+
function casesOf(m) {
|
|
268
|
+
const out = new Map();
|
|
269
|
+
for (const e of Object.values(m.step.state ?? {}))
|
|
270
|
+
for (let t = e; t.op === 'if' && out.size < 64; t = t.else)
|
|
271
|
+
if (!out.has(key(t.c)))
|
|
272
|
+
out.set(key(t.c), t.c);
|
|
273
|
+
return [...out.values()];
|
|
274
|
+
}
|
|
275
|
+
const caseAt = (cases, env, st, m) => {
|
|
276
|
+
for (const [i, c] of cases.entries()) {
|
|
277
|
+
try {
|
|
278
|
+
if (evalC(c, env, st, m))
|
|
279
|
+
return i;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return -1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return -1;
|
|
286
|
+
};
|
|
287
|
+
function constantsOf(m) {
|
|
288
|
+
const out = new Set([1, 2]);
|
|
289
|
+
const walk = (t) => {
|
|
290
|
+
if (!t || typeof t !== 'object')
|
|
291
|
+
return;
|
|
292
|
+
const o = t;
|
|
293
|
+
if (o.op === 'const' && typeof o.value === 'number' && o.value > 0 && o.value <= 64)
|
|
294
|
+
out.add(o.value);
|
|
295
|
+
if (o.op === 'mul' && typeof o.k === 'number' && o.k > 1 && o.k <= 64)
|
|
296
|
+
out.add(o.k);
|
|
297
|
+
for (const x of Object.values(o))
|
|
298
|
+
(Array.isArray(x) ? x : [x]).forEach(walk);
|
|
299
|
+
};
|
|
300
|
+
walk(m.step);
|
|
301
|
+
return [...out].sort((a, b) => a - b).slice(0, 6);
|
|
302
|
+
}
|
|
303
|
+
const dedupe = (cs) => [...new Map(cs.map(c => [key(c), c])).values()];
|
|
304
|
+
// The values of the machine's kinds a group may take: its naturals (an integer's two, x and x$neg), or a natural as an
|
|
305
|
+
// integer's positive part.
|
|
306
|
+
function valuesLike(g, names) {
|
|
307
|
+
const out = [];
|
|
308
|
+
for (const n of names) {
|
|
309
|
+
if (n.endsWith('$neg'))
|
|
310
|
+
continue;
|
|
311
|
+
const neg = names.includes(`${n}$neg`);
|
|
312
|
+
if (g.length === 2)
|
|
313
|
+
out.push(neg ? [v(n), v(`${n}$neg`)] : [v(n), k(0)], ...(neg ? [[v(`${n}$neg`), v(n)]] : []));
|
|
314
|
+
else
|
|
315
|
+
out.push([v(n)]);
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
function planOf(m, law) {
|
|
320
|
+
const vars = law.some, groups = groupsOf(vars), bound = new Set(vars), atoms = atomsOf(law.holds, bound);
|
|
321
|
+
const state = Object.keys(m.state), init = m.state;
|
|
322
|
+
const after = Object.fromEntries(state.map(n => [n, m.step.state?.[n] ?? v(n)]));
|
|
323
|
+
const keyNames = law.forall ? [law.forall, ...(law.keys ?? [])] : [];
|
|
324
|
+
const shifts = constantsOf(m);
|
|
325
|
+
const zero = (g) => g.map(() => k(0));
|
|
326
|
+
const terms = groups.map(g => dedupe([...solved(atoms, g), zero(g), ...valuesLike(g, [...keyNames, ...state, ...m.params])]));
|
|
327
|
+
// At the start: what P solves for over the start's values, 0, the params, the start's values.
|
|
328
|
+
const starts = groups.map(g => dedupe([...solved(atoms, g).map(c => c.map(t => substVarsE(t, init))), zero(g), ...valuesLike(g, m.params), ...valuesLike(g, state).map(c => c.map(t => substVarsE(t, init)))]));
|
|
329
|
+
// After a step: the ghost's value before, what P solves for over the state after, the request's values, shifts of
|
|
330
|
+
// the value before, the state before, the params, 0.
|
|
331
|
+
const steps = groups.map(g => {
|
|
332
|
+
const own = g.length === 2 ? [v(g[0]), v(g[1])] : [v(g[0])];
|
|
333
|
+
const shifted = shifts.flatMap(d => (g.length === 2 ? [[add(own[0], k(d)), own[1]], [own[0], add(own[1], k(d))]] : [[add(own[0], k(d))], [sub(own[0], k(d))]]));
|
|
334
|
+
// (and the value before moved by one of the request's values, or of the state's: a witness that counts them)
|
|
335
|
+
const by = (q, e) => (q === 1 ? e : { op: 'mul', k: q, a: e });
|
|
336
|
+
const moved = [1, 2, 3, 4].flatMap(q => valuesLike(g, [...m.inputs, ...state]).flatMap(c => (g.length === 2
|
|
337
|
+
? [[add(own[0], by(q, c[0])), add(own[1], by(q, c[1]))], [add(own[0], by(q, c[1])), add(own[1], by(q, c[0]))]]
|
|
338
|
+
: [[add(own[0], by(q, c[0]))], [sub(own[0], by(q, c[0]))]])));
|
|
339
|
+
const plain = dedupe([own, ...solved(atoms, g).map(c => c.map(t => substVarsE(t, after))), ...valuesLike(g, m.inputs), ...shifted, ...moved, ...valuesLike(g, [...state, ...m.params]), zero(g)]);
|
|
340
|
+
// (and the value before while it still works after the step, else one of those: P over the state after with every
|
|
341
|
+
// ghost's value before as the condition, a witness that moves only when it must, a quotient's, say)
|
|
342
|
+
if (readsMaps(law.holds))
|
|
343
|
+
return plain;
|
|
344
|
+
// (the condition: P's conjuncts that read this value and no other bound one, where there are some: a quotient's
|
|
345
|
+
// range, say; else all of P)
|
|
346
|
+
const mine = conjunctsOf(law.holds, bound).filter(c => { const ns = namesOf(c); return g.some(n => ns.includes(n)) && !ns.some(n => bound.has(n) && !g.includes(n)); });
|
|
347
|
+
const keep = substVars(mine.length ? { op: 'and', xs: mine } : law.holds, after);
|
|
348
|
+
return [...plain, ...plain.slice(1).map(t => t.map((x, j) => ({ op: 'if', c: keep, then: own[j], else: x })))];
|
|
349
|
+
});
|
|
350
|
+
const w = law.witness;
|
|
351
|
+
const given = !!(w?.terms || w?.ghost);
|
|
352
|
+
// A witness given is the only one tried: the kernel proves the law with it or refuses it.
|
|
353
|
+
if (w?.terms)
|
|
354
|
+
groups.forEach((g, i) => { terms[i] = [g.map(n => w.terms[n] ?? k(0))]; });
|
|
355
|
+
if (w?.ghost)
|
|
356
|
+
groups.forEach((g, i) => { starts[i] = [g.map(n => w.ghost[n]?.start ?? k(0))]; steps[i] = [g.map(n => w.ghost[n]?.step ?? v(n))]; });
|
|
357
|
+
return { law, groups, terms, starts, steps, pick: w?.ghost ? { mode: 'ghost', start: groups.map(() => 0), cases: new Map() } : { mode: 'term', at: groups.map(() => 0) }, seen: { term: [], start: [], step: new Map() }, given };
|
|
358
|
+
}
|
|
359
|
+
// The machine with every ghost the plans have chosen, and each law as the kernel proves it.
|
|
360
|
+
function built(m, plans, cases) {
|
|
361
|
+
const state = { ...m.state }, step = { ...m.step.state }, ghosts = [];
|
|
362
|
+
const laws = plans.map(p => {
|
|
363
|
+
const { law } = p, ex = { vars: law.some, body: law.holds };
|
|
364
|
+
const plain = { name: law.name, holds: law.holds, ...(law.forall ? { forall: law.forall } : {}), ...(law.keys ? { keys: law.keys } : {}), stated: law.stated ?? law.holds };
|
|
365
|
+
if (p.pick.mode === 'term') {
|
|
366
|
+
const pick = p.pick.at, sub = {};
|
|
367
|
+
p.groups.forEach((g, i) => g.forEach((n, j) => { sub[n] = p.terms[i][pick[i]][j]; }));
|
|
368
|
+
return { ...plain, holds: substVars(law.holds, sub), ex: { ...ex, sub } };
|
|
369
|
+
}
|
|
370
|
+
const { start, cases: chosen } = p.pick;
|
|
371
|
+
p.groups.forEach((g, i) => g.forEach((n, j) => {
|
|
372
|
+
state[n] = p.starts[i][start[i]][j];
|
|
373
|
+
const dflt = p.steps[i][chosen.get(-1)?.[i] ?? 0][j];
|
|
374
|
+
step[n] = [...chosen.entries()].filter(([c]) => c >= 0).sort((a, b) => b[0] - a[0]).reduce((acc, [c, at]) => ({ op: 'if', c: cases[c], then: p.steps[i][at[i]][j], else: acc }), dflt);
|
|
375
|
+
ghosts.push(n);
|
|
376
|
+
}));
|
|
377
|
+
return { ...plain, ex: { ...ex, ghost: true } };
|
|
378
|
+
});
|
|
379
|
+
return { machine: { ...m, state, step: { ...m.step, state: step } }, laws, ghosts };
|
|
380
|
+
}
|
|
381
|
+
// A state's values for every name, 0 for a name it doesn't give (as the prover reads a model).
|
|
382
|
+
const total = (w) => new Proxy(w, { get: (t, n) => (typeof n === 'string' ? (t[n] ?? 0) : undefined) });
|
|
383
|
+
const stOf = (w, m) => ({ scalars: total(w), maps: Object.fromEntries(Object.keys(m.maps ?? {}).map(n => [n, new Map()])), defaults: { ...m.maps } });
|
|
384
|
+
const evalOr = (e, env, m) => { try {
|
|
385
|
+
return evalE(e, total(env), stOf(env, m), m);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return undefined;
|
|
389
|
+
} };
|
|
390
|
+
const holdsOr = (c, env, m) => { try {
|
|
391
|
+
return evalC(c, total(env), stOf(env, m), m);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return undefined;
|
|
395
|
+
} };
|
|
396
|
+
// The candidates' indices, one per group, whose values satisfy `ok`: every group at the same index first (the lists
|
|
397
|
+
// are made alike, so one kind of witness for all: each quotient's, say), then one group moved from `from`, then in
|
|
398
|
+
// order of their sum (up to `cap` tried).
|
|
399
|
+
function firstFit(lists, ok, from, cap = 20000) {
|
|
400
|
+
const n = lists.length, sizes = lists.map(l => l.length);
|
|
401
|
+
const test = (pick) => ok(pick.map((x, j) => lists[j][x]));
|
|
402
|
+
if (n > 1) {
|
|
403
|
+
for (let x = 0; x < Math.min(...sizes); x++) {
|
|
404
|
+
const pick = lists.map(() => x);
|
|
405
|
+
if (test(pick))
|
|
406
|
+
return pick;
|
|
407
|
+
}
|
|
408
|
+
if (from)
|
|
409
|
+
for (let j = 0; j < n; j++)
|
|
410
|
+
for (let x = 0; x < sizes[j]; x++) {
|
|
411
|
+
const pick = [...from];
|
|
412
|
+
pick[j] = x;
|
|
413
|
+
if (test(pick))
|
|
414
|
+
return pick;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
let tried = 0;
|
|
418
|
+
for (let s = 0; s <= sizes.reduce((a, b) => a + b, 0); s++) {
|
|
419
|
+
const go = (i, left, at) => {
|
|
420
|
+
if (i === n - 1) {
|
|
421
|
+
if (left >= sizes[i])
|
|
422
|
+
return undefined;
|
|
423
|
+
const pick = [...at, left];
|
|
424
|
+
if (++tried > cap)
|
|
425
|
+
return undefined;
|
|
426
|
+
return ok(pick.map((x, j) => lists[j][x])) ? pick : undefined;
|
|
427
|
+
}
|
|
428
|
+
for (let x = 0; x <= Math.min(left, sizes[i] - 1); x++) {
|
|
429
|
+
const hit = go(i + 1, left - x, [...at, x]);
|
|
430
|
+
if (hit)
|
|
431
|
+
return hit;
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
};
|
|
435
|
+
if (!n)
|
|
436
|
+
return [];
|
|
437
|
+
const hit = go(0, s, []);
|
|
438
|
+
if (hit || tried > cap)
|
|
439
|
+
return hit;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
// The laws with existentials proven through witnesses: chosen per law by the counterexamples the kernel gives, then the
|
|
444
|
+
// whole set proven as plain laws of the machine with its ghosts.
|
|
445
|
+
export function proveSome(m, laws, opts = {}) {
|
|
446
|
+
const cases = casesOf(m), mapsRead = (l) => readsMaps(l.holds) || !!m.maps || !!m.inputMaps;
|
|
447
|
+
// Each law's bound naturals named apart from the machine's names and from each other's (each may become a ghost).
|
|
448
|
+
const taken = new Set([...m.params, ...m.inputs, ...Object.keys(m.state)]);
|
|
449
|
+
const plans = new Map();
|
|
450
|
+
laws.forEach((l, i) => {
|
|
451
|
+
if (!l.some?.length)
|
|
452
|
+
return;
|
|
453
|
+
const ren = {};
|
|
454
|
+
for (const n of l.some)
|
|
455
|
+
if (taken.has(n)) {
|
|
456
|
+
let alt = `${n.replace(/\$neg$/, '')}~${i}`;
|
|
457
|
+
if (n.endsWith('$neg'))
|
|
458
|
+
alt += '$neg';
|
|
459
|
+
ren[n] = v(alt);
|
|
460
|
+
}
|
|
461
|
+
const named = Object.keys(ren).length ? { ...l, holds: substVars(l.holds, ren), some: l.some.map(n => ren[n]?.name ?? n),
|
|
462
|
+
...(l.witness ? { witness: renamedWitness(l.witness, ren) } : {}) } : l;
|
|
463
|
+
for (const n of named.some)
|
|
464
|
+
taken.add(n);
|
|
465
|
+
plans.set(i, planOf(m, named));
|
|
466
|
+
});
|
|
467
|
+
const current = () => {
|
|
468
|
+
const b = built(m, [...plans.values()], cases);
|
|
469
|
+
const out = laws.map((l, i) => (plans.has(i) ? b.laws[[...plans.keys()].indexOf(i)] : l));
|
|
470
|
+
return { machine: b.machine, laws: out, ghosts: b.ghosts };
|
|
471
|
+
};
|
|
472
|
+
const rounds = opts.rounds ?? 24;
|
|
473
|
+
const log = globalThis.process?.env?.GDP_EXISTS_LOG === '1';
|
|
474
|
+
// Each law's witness until its goals are proven with every law assumed (choose); then the whole set, and where a
|
|
475
|
+
// later choice broke a law proven before (a witness term is a hypothesis of every law), that law's again.
|
|
476
|
+
const choose = (i, p, pass, first) => {
|
|
477
|
+
for (let round = 0;; round++) {
|
|
478
|
+
const now = current(), t0 = performance.now(), r = round === 0 && first ? { proven: false, ...first } : proveLaw(now.machine, now.laws, i);
|
|
479
|
+
if (log)
|
|
480
|
+
console.error(`exists: pass ${pass} round ${round} "${p.law.name}" ${p.pick.mode} ${describe(p, cases).slice(0, 160)}: ${r.proven ? 'proven' : `${r.goal} ${r.stuck}`} (${Math.round(performance.now() - t0)} ms)`);
|
|
481
|
+
if (r.proven)
|
|
482
|
+
return undefined;
|
|
483
|
+
const fail = (stuck) => ({ proven: false, nodes: 0, leaves: 0, checked: false, failed: { goal: r.goal, stuck, ...(r.witness ? { witness: r.witness } : {}) } });
|
|
484
|
+
// A witness given: the kernel proves the law with it, or refuses it here, with its counterexample.
|
|
485
|
+
if (p.given)
|
|
486
|
+
return { proven: false, nodes: 0, leaves: 0, checked: false, failed: { goal: r.goal, stuck: r.stuck, ...(r.witness ? { witness: r.witness } : {}), why: `the witness given for "${p.law.name}" does not prove it` } };
|
|
487
|
+
if (withinDeadline(Infinity) <= performance.now())
|
|
488
|
+
return fail(`the deadline passed choosing a witness for "${p.law.name}" (${r.stuck})`);
|
|
489
|
+
if (round >= rounds)
|
|
490
|
+
return fail(`no witness found for "${p.law.name}" in ${rounds} rounds (${r.stuck}): name one (exists(kind, f, witness))`);
|
|
491
|
+
if (r.stuck !== 'sat' || !r.witness) {
|
|
492
|
+
if (p.pick.mode === 'term' && !p.law.forall && !p.given) {
|
|
493
|
+
p.pick = { mode: 'ghost', start: p.groups.map(() => 0), cases: new Map() };
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
return fail(r.stuck);
|
|
497
|
+
}
|
|
498
|
+
const w = r.witness, atStart = r.goal.endsWith('@start'), blind = mapsRead(p.law);
|
|
499
|
+
// The state the goal fails at: the start (from the params), or the state after the step (from the state before,
|
|
500
|
+
// the request and the flags), every ghost as the machine has it now.
|
|
501
|
+
const env0 = { ...w };
|
|
502
|
+
if (p.law.forall)
|
|
503
|
+
env0[p.law.forall] = w[`${p.law.forall}#`] ?? 0;
|
|
504
|
+
const st = atStart ? now.machine.state : Object.fromEntries(Object.keys(now.machine.state).map(n => [n, now.machine.step.state?.[n] ?? v(n)]));
|
|
505
|
+
const after = { ...env0 };
|
|
506
|
+
for (const [n, e] of Object.entries(st)) {
|
|
507
|
+
const x = evalOr(e, env0, now.machine);
|
|
508
|
+
if (x !== undefined)
|
|
509
|
+
after[n] = x;
|
|
510
|
+
}
|
|
511
|
+
const at = (c, g, env) => Object.fromEntries(g.flatMap((gg, j) => gg.map((n, q) => [n, evalOr(c[j][q], env, now.machine) ?? -1])));
|
|
512
|
+
const sat = (vals, env) => !Object.values(vals).some(x => x < 0) && holdsOr(p.law.holds, { ...env, ...vals }, now.machine) === true;
|
|
513
|
+
if (p.pick.mode === 'term') {
|
|
514
|
+
p.seen.term.push(after);
|
|
515
|
+
const old = p.pick.at;
|
|
516
|
+
const fit = blind ? nextBlind(p.terms, old) : firstFit(p.terms, cs => p.seen.term.every(e => sat(at(cs, p.groups, e), e)), old);
|
|
517
|
+
if (fit && key(fit) !== key(old)) {
|
|
518
|
+
p.pick = { mode: 'term', at: fit };
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (!p.law.forall && !p.given) {
|
|
522
|
+
p.pick = { mode: 'ghost', start: p.groups.map(() => 0), cases: new Map() };
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
return fail(`no witness term found for "${p.law.name}" (${r.goal}): name one (exists(kind, f, witness))`);
|
|
526
|
+
}
|
|
527
|
+
if (atStart) {
|
|
528
|
+
p.seen.start.push(after);
|
|
529
|
+
const old = p.pick.start;
|
|
530
|
+
const fit = blind ? nextBlind(p.starts, old) : firstFit(p.starts, cs => p.seen.start.every(e => sat(at(cs, p.groups, e), e)), old);
|
|
531
|
+
if (!fit || key(fit) === key(old))
|
|
532
|
+
return fail(`no witness at the start found for "${p.law.name}": name one (exists(kind, f, witness))`);
|
|
533
|
+
p.pick.start = fit;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const c = caseAt(cases, env0, stOf(env0, now.machine), now.machine), seen = p.seen.step.get(c) ?? [];
|
|
537
|
+
seen.push({ before: env0, after });
|
|
538
|
+
p.seen.step.set(c, seen);
|
|
539
|
+
const old = p.pick.cases.get(c) ?? p.pick.cases.get(-1) ?? p.groups.map(() => 0);
|
|
540
|
+
const fit = blind ? nextBlind(p.steps, old) : firstFit(p.steps, cs => seen.every(e => sat(at(cs, p.groups, e.before), e.after)), old);
|
|
541
|
+
if (!fit || key(fit) === key(old))
|
|
542
|
+
return fail(`no witness after a step found for "${p.law.name}" (${cases[c] ? 'in one case of the step' : 'the step'}): name one (exists(kind, f, witness))`);
|
|
543
|
+
p.pick.cases.set(c, fit);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
// Each law's witness from its two goals (choose), then the whole set; where a later choice broke a law proven before
|
|
547
|
+
// (a witness term is a hypothesis of every law), that law's again from the whole set's counterexample, and so on.
|
|
548
|
+
for (const [i, p] of plans) {
|
|
549
|
+
const f = choose(i, p, 0);
|
|
550
|
+
if (f)
|
|
551
|
+
return f;
|
|
552
|
+
}
|
|
553
|
+
for (let pass = 1;; pass++) {
|
|
554
|
+
const { machine, laws: plain, ghosts } = current(), t0 = performance.now();
|
|
555
|
+
const out = proveLaws(machine, plain), done = { ...out, laws: out.laws ?? plain, machine, ghosts, witnesses: Object.fromEntries([...plans.values()].map(p => [p.law.name, describe(p, cases)])) };
|
|
556
|
+
if (log)
|
|
557
|
+
console.error(`exists: pass ${pass} the whole set: ${out.proven ? 'proven' : `${out.failed?.goal} ${out.failed?.stuck}`} (${Math.round(performance.now() - t0)} ms)`);
|
|
558
|
+
const j = out.proven || !out.failed || pass > 8 ? undefined : [...plans.keys()].find(k => out.failed.goal.startsWith(`${laws[k].name} @`));
|
|
559
|
+
if (j === undefined)
|
|
560
|
+
return done;
|
|
561
|
+
const f = choose(j, plans.get(j), pass, out.failed.stuck === 'sat' ? out.failed : undefined);
|
|
562
|
+
if (f)
|
|
563
|
+
return f;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const renamedWitness = (w, ren) => {
|
|
567
|
+
const name = (n) => ren[n]?.name ?? n;
|
|
568
|
+
return { ...(w.terms ? { terms: Object.fromEntries(Object.entries(w.terms).map(([n, t]) => [name(n), t])) } : {}),
|
|
569
|
+
...(w.ghost ? { ghost: Object.fromEntries(Object.entries(w.ghost).map(([n, g]) => [name(n), { start: g.start, step: substVarsE(g.step, ren) }])) } : {}) };
|
|
570
|
+
};
|
|
571
|
+
// With no model to read (a law over maps), the next candidate in order, the first group's fastest.
|
|
572
|
+
function nextBlind(lists, at) {
|
|
573
|
+
const next = [...at];
|
|
574
|
+
for (let i = 0; i < next.length; i++) {
|
|
575
|
+
if (next[i] + 1 < lists[i].length) {
|
|
576
|
+
next[i]++;
|
|
577
|
+
return next;
|
|
578
|
+
}
|
|
579
|
+
next[i] = 0;
|
|
580
|
+
}
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
// How a law's witness reads, for the verdict.
|
|
584
|
+
function describe(p, cases) {
|
|
585
|
+
// (each bound value as w, or w1, w2, ...: its own name is the builder's)
|
|
586
|
+
const names = new Map(p.groups.map((g, i) => [g[0], p.groups.length === 1 ? 'w' : `w${i + 1}`]));
|
|
587
|
+
const rn = (e) => substVarsE(e, Object.fromEntries(p.groups.flatMap(g => [[g[0], v(names.get(g[0]))], ...(g[1] ? [[g[1], v(`${names.get(g[0])}$neg`)]] : [])])));
|
|
588
|
+
const show = (c) => (c.length === 2 ? `${showE(rn(c[0]))} − ${showE(rn(c[1]))}` : showE(rn(c[0])));
|
|
589
|
+
const name = (g) => names.get(g[0]);
|
|
590
|
+
if (p.pick.mode === 'term') {
|
|
591
|
+
const at = p.pick.at;
|
|
592
|
+
return p.groups.map((g, i) => `${name(g)} = ${show(p.terms[i][at[i]])}`).join(', ');
|
|
593
|
+
}
|
|
594
|
+
const { start, cases: cs } = p.pick;
|
|
595
|
+
return p.groups.map((g, i) => `${name(g)} starts at ${show(p.starts[i][start[i]])}; after a step ${[...cs.entries()].filter(([c]) => c >= 0).map(([c, at]) => `${show(p.steps[i][at[i]])} where ${showC(cases[c])}`).join(', ')}${cs.size > (cs.has(-1) ? 1 : 0) ? ', else ' : ''}${show(p.steps[i][cs.get(-1)?.[i] ?? 0])}`).join('; ');
|
|
596
|
+
}
|
|
597
|
+
export function showE(e) {
|
|
598
|
+
switch (e.op) {
|
|
599
|
+
case 'var': return e.name;
|
|
600
|
+
case 'const': return String(e.value);
|
|
601
|
+
case 'add': return `${showE(e.a)} + ${showE(e.b)}`;
|
|
602
|
+
case 'sub': return `(${showE(e.a)} ∸ ${showE(e.b)})`;
|
|
603
|
+
case 'mul': return `${e.k}·${showE(e.a)}`;
|
|
604
|
+
case 'prod': return `${showE(e.a)}·${showE(e.b)}`;
|
|
605
|
+
case 'min':
|
|
606
|
+
case 'max': return `${e.op}(${showE(e.a)}, ${showE(e.b)})`;
|
|
607
|
+
case 'if': return `(${showC(e.c)} ? ${showE(e.then)} : ${showE(e.else)})`;
|
|
608
|
+
case 'get': return `${e.map}[${showE(e.key)}]`;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
export function showC(c) {
|
|
612
|
+
switch (c.op) {
|
|
613
|
+
case '<':
|
|
614
|
+
case '<=':
|
|
615
|
+
case '=': return `${showE(c.a)} ${c.op} ${showE(c.b)}`;
|
|
616
|
+
case 'and': return c.xs.map(showC).join(' ∧ ');
|
|
617
|
+
case 'or': return `(${c.xs.map(showC).join(' ∨ ')})`;
|
|
618
|
+
case 'not': return `¬(${showC(c.x)})`;
|
|
619
|
+
case 'flag': return c.name;
|
|
620
|
+
case 'all': return `∀${c.key}. ${showC(c.c)}`;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// One law's two goals with its existentials' witnesses (core/infer.ts adds laws one at a time): the witnesses chosen
|
|
624
|
+
// for the whole set (proveSome), the law's goals proven over that machine.
|
|
625
|
+
export const someLaws = (laws) => laws.some(l => l.some?.length);
|
package/dist/core/infer.d.ts
CHANGED
|
@@ -3,12 +3,16 @@ import { type StepLaw, type Verdict, proveRule } from './dsl.js';
|
|
|
3
3
|
type AnyRule = Parameters<typeof proveRule>[0];
|
|
4
4
|
export type InferOptions = {
|
|
5
5
|
ms?: number;
|
|
6
|
+
runs?: number;
|
|
6
7
|
runsMs?: number;
|
|
8
|
+
timed?: boolean;
|
|
7
9
|
seen?: State[];
|
|
8
10
|
constants?: number[];
|
|
9
11
|
maxCandidates?: number;
|
|
10
12
|
first?: Verdict;
|
|
11
13
|
progress?: (phase: string, info: Record<string, number | string>) => void;
|
|
14
|
+
existentials?: boolean;
|
|
15
|
+
remainders?: boolean;
|
|
12
16
|
};
|
|
13
17
|
export type Found = {
|
|
14
18
|
law: Law;
|
|
@@ -23,7 +27,16 @@ export type Inferred = Verdict & {
|
|
|
23
27
|
alive: number;
|
|
24
28
|
proofs: number;
|
|
25
29
|
ms: number;
|
|
30
|
+
stopped: boolean;
|
|
26
31
|
};
|
|
27
32
|
};
|
|
33
|
+
export declare const inferRates: {
|
|
34
|
+
tries: number;
|
|
35
|
+
rows: number;
|
|
36
|
+
evals: number;
|
|
37
|
+
watched: number;
|
|
38
|
+
work: number;
|
|
39
|
+
};
|
|
40
|
+
export declare const inferRuns = 16000;
|
|
28
41
|
export declare function inferLaws(target: AnyRule | Machine, laws: Law[], stepLaws?: StepLaw[], opts?: InferOptions): Inferred;
|
|
29
42
|
export {};
|