gdp-wired 0.17.0 → 0.18.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 +10 -3
- package/dist/core/cycles.d.ts +73 -0
- package/dist/core/cycles.js +899 -0
- package/dist/core/cyclic.d.ts +18 -2
- package/dist/core/cyclic.js +488 -43
- package/dist/core/data.d.ts +258 -0
- package/dist/core/data.js +1858 -0
- package/dist/core/dsl.d.ts +7 -1
- package/dist/core/dsl.js +18 -1
- package/dist/kernel/prove.d.ts +5 -0
- package/dist/kernel/prove.js +160 -5
- package/dist/lean-kernel/emit.d.ts +61 -1
- package/dist/lean-kernel/emit.js +211 -35
- package/dist/lean-kernel/library.js +2 -0
- package/package.json +10 -2
package/dist/core/cyclic.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { substC, substE } from "../kernel/kernel.js";
|
|
2
|
-
import { descentGoal, mainGoal, premiseGoal, refuseCyclic } from "../kernel/cyclic.js";
|
|
3
|
-
import { cellProducts, grow } from "../kernel/prove.js";
|
|
2
|
+
import { baseHyps, descentGoal, instHyps, mainGoal, premiseGoal, refuseCyclic } from "../kernel/cyclic.js";
|
|
3
|
+
import { cellProducts, grow, profile, usedHyps, widen } from "../kernel/prove.js";
|
|
4
4
|
const v = (name) => ({ op: 'var', name });
|
|
5
5
|
const k = (value) => ({ op: 'const', value });
|
|
6
6
|
const add = (a, b) => ({ op: 'add', a, b });
|
|
@@ -17,46 +17,320 @@ const vars = (s) => ({ vars: s, maps: {}, flags: {} });
|
|
|
17
17
|
const omega = v('O$');
|
|
18
18
|
export const pair = (u, key) => add(u, { op: 'prod', a: key, b: omega });
|
|
19
19
|
// Every goal grown, then the whole checked by the kernel.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// GDP_CYCLIC_PROF=1: each comparison's line also has where the cyclic proof's time went, by goal kind (main, premise,
|
|
21
|
+
// descent, the kernel's check): ms, goals, hypotheses, solver and row ms.
|
|
22
|
+
export const cyclicProf = {};
|
|
23
|
+
const profOn = () => env()?.GDP_CYCLIC_PROF === '1';
|
|
24
|
+
function timedKind(kind, hyps, f) {
|
|
25
|
+
if (!profOn())
|
|
26
|
+
return f();
|
|
27
|
+
const was = profile.on, s0 = profile.solveMs, r0 = profile.rowsMs, t0 = performance.now();
|
|
28
|
+
profile.on = true;
|
|
29
|
+
try {
|
|
30
|
+
return f();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
profile.on = was;
|
|
34
|
+
const p = (cyclicProf[kind] ??= { ms: 0, n: 0, hyps: 0, solveMs: 0, rowsMs: 0 });
|
|
35
|
+
p.ms += performance.now() - t0;
|
|
36
|
+
p.n++;
|
|
37
|
+
p.hyps += hyps;
|
|
38
|
+
p.solveMs += profile.solveMs - s0;
|
|
39
|
+
p.rowsMs += profile.rowsMs - r0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Each goal grown first on what it reads: its node's hypotheses and its own last ones (a use's guard, the conclusion
|
|
43
|
+
// negated) kept whole, and of the rest (the facts at the node's terms, the uses) only the parts it reaches: a
|
|
44
|
+
// conjunction's conjuncts (under a guard, as a step's equations are, one per state variable) each a part of its own; an
|
|
45
|
+
// equation that gives a cell (a state variable's value at t, a function's at a key) reached when its cell is, and then
|
|
46
|
+
// its terms' cells and its guard's; any other part reached when it shares a cell with what is, or reads none. The
|
|
47
|
+
// weaker goal's tree is the whole goal's (`widen`); where the weaker goal isn't refuted, the whole is grown as before.
|
|
48
|
+
// GDP_CYCLIC_SLICE=0 grows the whole goal only.
|
|
49
|
+
// A cell by what it may be equal to: a run's map read at time u and any key (two keys the prover may prove equal) is
|
|
50
|
+
// one, by its map and u; a function's read at any key is one, by the function; a run's other reads are themselves.
|
|
51
|
+
const cellsIn = (t, out = new Set()) => {
|
|
52
|
+
if (t && typeof t === 'object') {
|
|
53
|
+
const o = t, map = o.map;
|
|
54
|
+
if (o.op === 'get') {
|
|
55
|
+
const key = o.key;
|
|
56
|
+
out.add(key.op === 'add' && key.b.op === 'prod' && JSON.stringify(key.b.b) === JSON.stringify(omega) ? `${map}@${JSON.stringify(key.a)}` : map.endsWith('@') ? JSON.stringify(o) : map);
|
|
57
|
+
}
|
|
58
|
+
for (const val of Object.values(o))
|
|
59
|
+
cellsIn(val, out);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
};
|
|
63
|
+
function partsOf(c, guards = [], out = []) {
|
|
64
|
+
if (c.op === 'and')
|
|
65
|
+
c.xs.forEach(x => partsOf(x, guards, out));
|
|
66
|
+
else if (c.op === 'or' && c.xs.length === 2 && c.xs[0].op === 'not')
|
|
67
|
+
partsOf(c.xs[1], [...guards, c.xs[0]], out);
|
|
68
|
+
else
|
|
69
|
+
out.push({ c, guards });
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function rebuild(c, keep, at = { j: 0 }) {
|
|
73
|
+
if (c.op === 'and') {
|
|
74
|
+
const xs = c.xs.flatMap(x => { const y = rebuild(x, keep, at); return y ? [y] : []; });
|
|
75
|
+
return xs.length ? and(...xs) : undefined;
|
|
76
|
+
}
|
|
77
|
+
if (c.op === 'or' && c.xs.length === 2 && c.xs[0].op === 'not') {
|
|
78
|
+
const b = rebuild(c.xs[1], keep, at);
|
|
79
|
+
return b && { op: 'or', xs: [c.xs[0], b] };
|
|
80
|
+
}
|
|
81
|
+
return keep(at.j++) ? c : undefined;
|
|
82
|
+
}
|
|
83
|
+
// The hypotheses with a part the goal reaches (whole ones included).
|
|
84
|
+
export function reached(g, whole) { return slice(g, whole, true); }
|
|
85
|
+
export function slice(g, whole, hypsOnly = false) {
|
|
86
|
+
const reach = new Set(), parts = g.hyps.map(h => partsOf(h));
|
|
87
|
+
const pieces = [];
|
|
88
|
+
g.hyps.forEach((h, i) => {
|
|
89
|
+
if (whole(i)) {
|
|
90
|
+
cellsIn(h, reach);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
parts[i].forEach(({ c, guards }, j) => {
|
|
94
|
+
// (an equation between two reads, a law's, gives neither: it is reached by either)
|
|
95
|
+
const gives = c.op === '=' && c.a.op === 'get' && c.b.op !== 'get' ? [...cellsIn(c.a)][0] : undefined, cells = cellsIn(c.op === '=' && gives ? c.b : c);
|
|
96
|
+
for (const x of guards)
|
|
97
|
+
cellsIn(x, cells);
|
|
98
|
+
pieces.push({ c, guards, i, j, gives, cells });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
const kept = new Set();
|
|
102
|
+
for (let grew = true; grew;) {
|
|
103
|
+
grew = false;
|
|
104
|
+
for (const [q, p] of pieces.entries()) {
|
|
105
|
+
if (kept.has(q) || (p.gives ? !reach.has(p.gives) : p.cells.size && ![...p.cells].some(c => reach.has(c))))
|
|
106
|
+
continue;
|
|
107
|
+
kept.add(q);
|
|
108
|
+
grew = true;
|
|
109
|
+
for (const c of p.cells)
|
|
110
|
+
reach.add(c);
|
|
111
|
+
if (p.gives)
|
|
112
|
+
reach.add(p.gives);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (hypsOnly)
|
|
116
|
+
return new Set(g.hyps.flatMap((_, i) => (whole(i) || pieces.some((p, q) => p.i === i && kept.has(q)) ? [i] : [])));
|
|
117
|
+
// The equations that give a cell the conclusion reads (a step's, the start's) join the conclusion, under their guards:
|
|
118
|
+
// the prover splits the conclusion's atoms first, as the usual rule does a law read after the step.
|
|
119
|
+
const last = g.hyps.length - 1, concl = whole(last) ? cellsIn(g.hyps[last]) : new Set();
|
|
120
|
+
const joins = (p) => !!p.gives && concl.has(p.gives);
|
|
121
|
+
if (kept.size === pieces.length && !pieces.some(joins))
|
|
122
|
+
return undefined;
|
|
123
|
+
const stays = new Set(), joined = [];
|
|
124
|
+
for (const [q, p] of pieces.entries())
|
|
125
|
+
if (kept.has(q)) {
|
|
126
|
+
if (joins(p))
|
|
127
|
+
joined.push(p.guards.reduceRight((acc, x) => ({ op: 'or', xs: [x, acc] }), p.c));
|
|
128
|
+
else
|
|
129
|
+
stays.add(`${p.i}:${p.j}`);
|
|
130
|
+
}
|
|
131
|
+
const from = [], joinedFrom = [...new Set(pieces.flatMap((p, q) => (kept.has(q) && joins(p) ? [p.i] : [])))];
|
|
132
|
+
const hyps = g.hyps.flatMap((h, i) => {
|
|
133
|
+
const r = whole(i) || parts[i].every((_, j) => stays.has(`${i}:${j}`)) ? h : rebuild(h, j => stays.has(`${i}:${j}`));
|
|
134
|
+
if (r)
|
|
135
|
+
from.push([i]);
|
|
136
|
+
return r ? [r] : [];
|
|
137
|
+
});
|
|
138
|
+
if (joined.length) {
|
|
139
|
+
hyps[hyps.length - 1] = and(hyps.at(-1), ...joined);
|
|
140
|
+
from[from.length - 1].push(...joinedFrom);
|
|
141
|
+
}
|
|
142
|
+
return { name: g.name, hyps, from };
|
|
143
|
+
}
|
|
144
|
+
let sliceNow = false;
|
|
145
|
+
const sliceOn = () => sliceNow && env()?.GDP_CYCLIC_SLICE !== '0';
|
|
146
|
+
const afterOn = () => env()?.GDP_CYCLIC_AFTER !== '0';
|
|
147
|
+
const stepNodesOn = () => env()?.GDP_CYCLIC_STEP !== '0';
|
|
148
|
+
// Trees already grown, by their goal (a node met again: a recurrence's lemmas in its laws between, a machine proven again),
|
|
149
|
+
// with the facts and uses a node's own goal kept.
|
|
150
|
+
const grownBefore = new Map();
|
|
151
|
+
// `order`: the order the nodes are grown in, by default those with a weak variant first (a variant that uses a weak node
|
|
152
|
+
// is passed over).
|
|
153
|
+
export function proveCyclic(facts, specs, order = [...specs.keys()].sort((i, j) => Number(!specs[i].variants?.some(x => x.weak)) - Number(!specs[j].variants?.some(x => x.weak)))) {
|
|
154
|
+
const nodes = [], shapes = [...specs], weak = new Set();
|
|
155
|
+
let later, refuted = false;
|
|
156
|
+
// A goal's tree, and the hypotheses its leaves read (of the whole goal, a weaker goal's read through where they came from).
|
|
157
|
+
const grown = (g, node, own) => {
|
|
158
|
+
const was = cellProducts.on, kind = g.name.endsWith('its rank below') ? 'descent' : g.name.includes(': use of node') ? 'premise' : 'main';
|
|
24
159
|
cellProducts.on = true;
|
|
25
|
-
let o;
|
|
26
160
|
try {
|
|
27
|
-
|
|
161
|
+
const sliced = sliceOn() ? slice(g, own) : undefined, part = sliced && hide(sliced);
|
|
162
|
+
if (part) {
|
|
163
|
+
// (within two seconds: a weaker goal no easier than the whole is left to the whole)
|
|
164
|
+
const t0 = performance.now(), p = timedKind(`${kind} sliced`, part.hyps.length, () => grow(part, new Map(), { nodes: 0, until: performance.now() + Math.min(2_000, later ?? 2_000) }));
|
|
165
|
+
if (env()?.GDP_CYCLIC_DEBUG)
|
|
166
|
+
console.error(`[cyc] ${g.name}: ${part.hyps.length} hyps${part !== sliced ? ' (opaque)' : ''}, ${'tree' in p ? `${p.nodes} nodes` : p.stuck}, ${Math.round(performance.now() - t0)} ms`);
|
|
167
|
+
if ('tree' in p)
|
|
168
|
+
return { tree: p.tree, from: part, used: () => new Set([...usedHyps(part, p.tree)].flatMap(i => part.from[i])) };
|
|
169
|
+
if (part !== sliced) {
|
|
170
|
+
const q = timedKind(`${kind} sliced`, sliced.hyps.length, () => grow(sliced, new Map(), { nodes: 0, until: performance.now() + Math.min(2_000, later ?? 2_000) }));
|
|
171
|
+
if ('tree' in q)
|
|
172
|
+
return { tree: q.tree, from: sliced, used: () => new Set([...usedHyps(sliced, q.tree)].flatMap(i => sliced.from[i])) };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const o = timedKind(kind, g.hyps.length, () => grow(g, new Map(), { nodes: 0, until: performance.now() + (later ?? Math.max(20_000, 1_000 * g.hyps.length)) }));
|
|
176
|
+
return 'tree' in o ? { tree: o.tree, from: g, used: () => usedHyps(g, o.tree) } : { proven: false, checked: false, node, goal: g.name, stuck: o.stuck };
|
|
28
177
|
}
|
|
29
178
|
finally {
|
|
30
179
|
cellProducts.on = was;
|
|
31
180
|
}
|
|
32
|
-
return 'tree' in o ? o.tree : { proven: false, checked: false, node, goal: g.name, stuck: o.stuck };
|
|
33
181
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
182
|
+
const withProducts = (f) => { const was = cellProducts.on; cellProducts.on = true; try {
|
|
183
|
+
return f();
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
cellProducts.on = was;
|
|
187
|
+
} };
|
|
188
|
+
// The tree made the goal's own (a weaker goal's widened), or grown on the goal itself where that fails.
|
|
189
|
+
const own = (r, g) => (r.from === g ? r.tree : timedKind('widen', 0, () => widen(r.from, g, r.tree, opaqueOf.get(r.from))));
|
|
190
|
+
const whole = (g, node, mine) => {
|
|
191
|
+
const key = JSON.stringify(g.hyps), before = grownBefore.get(key);
|
|
192
|
+
if (before)
|
|
193
|
+
return before.tree;
|
|
194
|
+
const r = grown(g, node, mine);
|
|
195
|
+
if ('proven' in r)
|
|
196
|
+
return r;
|
|
197
|
+
const t = own(r, g) ?? (() => { const o = grow(g); return 'tree' in o ? o.tree : undefined; })();
|
|
198
|
+
if (!t)
|
|
199
|
+
return { proven: false, checked: false, node, goal: g.name, stuck: 'unknown' };
|
|
200
|
+
if (grownBefore.size >= 4096)
|
|
201
|
+
grownBefore.clear();
|
|
202
|
+
grownBefore.set(key, { tree: t });
|
|
203
|
+
return t;
|
|
204
|
+
};
|
|
205
|
+
// A goal with the node's opaque terms read as variables (z$0, z$1, ...), remembered with them for `widen`.
|
|
206
|
+
const opaqueOf = new WeakMap();
|
|
207
|
+
let hidden = [];
|
|
208
|
+
const hide = (g) => {
|
|
209
|
+
if (!hidden.length)
|
|
210
|
+
return g;
|
|
211
|
+
let text = JSON.stringify(g.hyps);
|
|
212
|
+
const names = {};
|
|
213
|
+
hidden.forEach((e, i) => { const id = JSON.stringify(e); if (text.includes(id)) {
|
|
214
|
+
text = text.replaceAll(id, JSON.stringify(v(`z$${i}`)));
|
|
215
|
+
names[`z$${i}`] = e;
|
|
216
|
+
} });
|
|
217
|
+
if (!Object.keys(names).length)
|
|
218
|
+
return g;
|
|
219
|
+
const out = { ...g, hyps: JSON.parse(text) };
|
|
220
|
+
opaqueOf.set(out, names);
|
|
221
|
+
return out;
|
|
222
|
+
};
|
|
223
|
+
// A node's goal grown; then the node keeps only the facts and uses its tree reads (the fewer, the fewer hypotheses its
|
|
224
|
+
// goal has for the kernel to read at every leaf, and the fewer uses to prove below); then each use's goals.
|
|
225
|
+
const attempt = ({ opaque, variants: _v, weak: _w, slice: sl, ...n0 }) => {
|
|
226
|
+
hidden = opaque ?? [];
|
|
227
|
+
sliceNow = !!sl;
|
|
228
|
+
// A goal's own hypotheses: the node's, and those after its facts and uses.
|
|
229
|
+
const n1 = { ...n0, insts: n0.insts.filter(p => facts[p.fact]) };
|
|
230
|
+
const base = baseHyps(facts, n1).length, mine = (tail) => (i) => i < n1.hyps.length || i >= base + tail;
|
|
231
|
+
const g = mainGoal(facts, shapes, n1), key = JSON.stringify(g.hyps), before = grownBefore.get(key);
|
|
232
|
+
let n = n1, main, src;
|
|
233
|
+
if (before) {
|
|
234
|
+
n = { ...n1, insts: before.insts?.map(i => n1.insts[i]) ?? n1.insts, uses: before.uses?.map(j => n1.uses[j]) ?? n1.uses };
|
|
235
|
+
main = before.tree;
|
|
236
|
+
src = { tree: main, from: mainGoal(facts, shapes, n) };
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// First the node with only the facts and uses its goal reaches, its goal grown as it is (within two seconds).
|
|
240
|
+
const h0 = n1.hyps.length, keep = sliceOn() ? reached(g, mine(n1.uses.length)) : undefined;
|
|
241
|
+
const n2 = keep && { ...n1, insts: n1.insts.filter((_, i) => keep.has(h0 + i)), uses: n1.uses.filter((_, j) => keep.has(base + j)) };
|
|
242
|
+
if (n2 && (n2.insts.length < n1.insts.length || n2.uses.length < n1.uses.length)) {
|
|
243
|
+
const g2 = mainGoal(facts, shapes, n2), part = hide(g2);
|
|
244
|
+
const p = timedKind('main selected', g2.hyps.length, () => withProducts(() => grow(part, new Map(), { nodes: 0, until: performance.now() + Math.min(2_000, later ?? 2_000) })));
|
|
245
|
+
if (env()?.GDP_CYCLIC_DEBUG)
|
|
246
|
+
console.error(`[cyc] ${g.name}: ${g2.hyps.length} of ${g.hyps.length} hyps selected, ${'tree' in p ? `${p.nodes} nodes` : p.stuck}`);
|
|
247
|
+
const t = 'tree' in p ? own({ tree: p.tree, from: part }, g2) : undefined;
|
|
248
|
+
if (t && 'tree' in p) {
|
|
249
|
+
n = n2;
|
|
250
|
+
main = t;
|
|
251
|
+
src = { tree: p.tree, from: part };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Then the whole node, its goal sliced; it keeps the facts and uses its tree reads.
|
|
255
|
+
if (!main) {
|
|
256
|
+
const r = grown(g, n1.name, mine(n1.uses.length));
|
|
257
|
+
if ('proven' in r)
|
|
258
|
+
return r;
|
|
259
|
+
const used = sliceOn() ? timedKind('used', 0, r.used) : undefined;
|
|
260
|
+
const trim = used ? { ...n1, insts: n1.insts.filter((_, i) => used.has(h0 + i)), uses: n1.uses.filter((_, j) => used.has(base + j)) } : n1;
|
|
261
|
+
const g2 = trim.insts.length < n1.insts.length || trim.uses.length < n1.uses.length ? mainGoal(facts, shapes, trim) : g;
|
|
262
|
+
const t2 = own(r, g2);
|
|
263
|
+
main = t2 ?? own(r, g) ?? (() => { const o = grow(g); return 'tree' in o ? o.tree : undefined; })();
|
|
264
|
+
if (!main)
|
|
265
|
+
return { proven: false, checked: false, node: n1.name, goal: g.name, stuck: 'unknown' };
|
|
266
|
+
if (t2)
|
|
267
|
+
n = trim;
|
|
268
|
+
src = { tree: r.tree, from: r.from };
|
|
269
|
+
}
|
|
270
|
+
if (grownBefore.size >= 4096)
|
|
271
|
+
grownBefore.clear();
|
|
272
|
+
grownBefore.set(key, { tree: main, insts: n.insts.map(p => n1.insts.indexOf(p)), uses: n.uses.map(u => n1.uses.indexOf(u)) });
|
|
273
|
+
}
|
|
274
|
+
hidden = [];
|
|
275
|
+
// Each use's goals; where one isn't proven with the facts the node kept, again with all of them.
|
|
276
|
+
const usesOf = (n) => {
|
|
277
|
+
const nb = baseHyps(facts, n).length, mine2 = (i) => i < n.hyps.length || i >= nb, out = [];
|
|
278
|
+
for (const u of n.uses) {
|
|
279
|
+
const m = shapes[u.node];
|
|
280
|
+
if (!m)
|
|
281
|
+
return { proven: false, checked: false, node: n.name, goal: `use of node ${u.node}`, stuck: 'no such node' };
|
|
282
|
+
const premises = [];
|
|
283
|
+
for (const [i, c] of m.hyps.entries()) {
|
|
284
|
+
const t = whole(premiseGoal(facts, n, u, c, i), n.name, mine2);
|
|
285
|
+
if ('proven' in t)
|
|
286
|
+
return t;
|
|
287
|
+
premises.push(t);
|
|
288
|
+
}
|
|
289
|
+
const descent = whole(descentGoal(facts, n, u, m), n.name, mine2);
|
|
290
|
+
if ('proven' in descent)
|
|
291
|
+
return descent;
|
|
292
|
+
out.push({ ...u, premises, descent });
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
};
|
|
296
|
+
const uses = usesOf(n);
|
|
297
|
+
if (!('proven' in uses))
|
|
298
|
+
return { ...n, uses, main: main };
|
|
299
|
+
if (n.insts.length === n1.insts.length || !src)
|
|
300
|
+
return uses;
|
|
301
|
+
const n3 = { ...n, insts: n1.insts }, main3 = own(src, mainGoal(facts, shapes, n3)), uses3 = main3 ? usesOf(n3) : uses;
|
|
302
|
+
return main3 && !('proven' in uses3) ? { ...n3, uses: uses3, main: main3 } : uses;
|
|
303
|
+
};
|
|
304
|
+
for (const i of order) {
|
|
305
|
+
let failed;
|
|
306
|
+
for (const [j, spec] of [specs[i], ...(specs[i].variants ?? [])].entries()) {
|
|
307
|
+
// (a variant after the first within a few seconds a goal, one second once a node is refuted: a variant the first
|
|
308
|
+
// wasn't proven by is seldom needed, and seldom proves a node where another is refuted)
|
|
309
|
+
later = j ? (refuted ? 1_000 : 3_000) : undefined;
|
|
310
|
+
if (spec.uses.some(u => weak.has(u.node)))
|
|
311
|
+
continue;
|
|
312
|
+
const r = attempt(spec);
|
|
313
|
+
if ('proven' in r) {
|
|
314
|
+
failed ??= r;
|
|
315
|
+
if (r.stuck === 'sat')
|
|
316
|
+
refuted = true;
|
|
317
|
+
continue;
|
|
49
318
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
319
|
+
nodes[i] = r;
|
|
320
|
+
shapes[i] = spec;
|
|
321
|
+
failed = undefined;
|
|
322
|
+
if (spec.weak)
|
|
323
|
+
weak.add(i);
|
|
324
|
+
break;
|
|
54
325
|
}
|
|
55
|
-
|
|
326
|
+
if (failed || !nodes[i])
|
|
327
|
+
return failed ?? { proven: false, checked: false, node: specs[i].name, goal: specs[i].name, stuck: 'unknown' };
|
|
56
328
|
}
|
|
57
|
-
const proof = { facts, nodes }, why = refuseCyclic(proof);
|
|
329
|
+
const proof = { facts, nodes }, why = timedKind('check', 0, () => refuseCyclic(proof));
|
|
58
330
|
return { proven: true, checked: why === undefined, proof, ...(why ? { why } : {}) };
|
|
59
331
|
}
|
|
332
|
+
// An instance's nodes, and their variants, grown first on what their goals read.
|
|
333
|
+
const sliced = (specs) => specs.map(sp => ({ ...sp, slice: true, ...(sp.variants ? { variants: sp.variants.map(x => ({ ...x, slice: true })) } : {}) }));
|
|
60
334
|
// The facts of a machine's functions (laws about every key of them), as cyclic facts.
|
|
61
335
|
const factOf = (l) => ({ name: l.name, keys: [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])], holds: l.holds });
|
|
62
336
|
export function recurNodes(r) {
|
|
@@ -81,11 +355,41 @@ export function recurNodes(r) {
|
|
|
81
355
|
insts: [{ fact: 1, at: [less1(y)] }],
|
|
82
356
|
uses: [...r.between.map((_, j) => ({ node: n + j, subst: { y: less1(y) }, guard: le(add(x, k(1)), y) })), ...lemmaUses(x, yes), ...lemmaUses(y, yes), ...lemmaUses(less1(y), le(k(1), y))],
|
|
83
357
|
});
|
|
84
|
-
|
|
358
|
+
if (!stepNodesOn())
|
|
359
|
+
return { facts, specs: sliced(withReads(facts, specs)) };
|
|
360
|
+
// Each lemma and law twice, as a machine's laws are (GDP_CYCLIC_STEP=0 states the first alone): at x (rank (0, x, 1)),
|
|
361
|
+
// from f(0) or from the lemma after a step to x, rank (0, x, 0), read at next(x − 1, f(x − 1)) (the usual rule's step
|
|
362
|
+
// goal) and using every lemma at x − 1; a law between x and y likewise at y (rank (1, y, ·)), after a step to y using
|
|
363
|
+
// the laws at y − 1 and the lemmas at x and y − 1.
|
|
364
|
+
const b = r.between.length, step = (e) => substE(r.next, vars({ x: less1(e), prev: get(f, less1(e)) }));
|
|
365
|
+
const atStep = (l) => substC(l.holds, vars({ x, fx: step(x) }));
|
|
366
|
+
const lemmaAfter = r.lemmas.map((l) => {
|
|
367
|
+
const name = `${r.name}: ${l.name}, after a step`, hyps = [le(k(1), x)];
|
|
368
|
+
return { name, hyps, concl: atStep(l), rank: [k(0), x, k(0)], insts: [], uses: lemmaUses(less1(x), yes), variants: [{ name, hyps, concl: yes, rank: [k(0), x, k(0)], insts: [], uses: [], weak: true }] };
|
|
369
|
+
});
|
|
370
|
+
const betweenAfter = r.between.map((l) => {
|
|
371
|
+
const name = `${r.name}: ${l.name}, after a step`, hyps = [le(add(x, k(1)), y)];
|
|
372
|
+
return { name, hyps, concl: substC(l.holds, vars({ x, fx: get(f, x), y, fy: step(y) })), rank: [k(1), y, k(0)], insts: [],
|
|
373
|
+
uses: [...r.between.map((_, j) => ({ node: n + j, subst: { y: less1(y) }, guard: yes })), ...lemmaUses(x, yes), ...lemmaUses(less1(y), yes)],
|
|
374
|
+
variants: [{ name, hyps, concl: yes, rank: [k(1), y, k(0)], insts: [], uses: [], weak: true }] };
|
|
375
|
+
});
|
|
376
|
+
const own = specs.map((sp, i) => {
|
|
377
|
+
const lemma = i < n, a = { ...sp, rank: [...sp.rank, k(1)], insts: lemma ? sp.insts : [{ fact: 1, at: [less1(y)] }],
|
|
378
|
+
uses: lemma ? [{ node: n + b + i, subst: {}, guard: le(k(1), x) }] : [{ node: 2 * n + b + (i - n), subst: {}, guard: le(add(x, k(1)), y) }, ...lemmaUses(x, yes), ...lemmaUses(y, yes)],
|
|
379
|
+
variants: [{ ...sp, rank: [...sp.rank, k(1)] }] };
|
|
380
|
+
return a;
|
|
381
|
+
});
|
|
382
|
+
const all = withReads(facts, [...own, ...lemmaAfter, ...betweenAfter]);
|
|
383
|
+
for (const [i, a] of all.slice(0, n + b).entries()) {
|
|
384
|
+
const o = shared(facts, a, all[n + b + i].concl);
|
|
385
|
+
if (o.length)
|
|
386
|
+
all[i] = { ...a, opaque: o };
|
|
387
|
+
}
|
|
388
|
+
return { facts, specs: sliced(all.map(sp => (sp.variants ? { ...sp, variants: withReads(facts, sp.variants, all) } : sp))) };
|
|
85
389
|
}
|
|
86
390
|
// Function facts at the keys a node's goal reads them at: each fact about every key of a function read f[k] (the key
|
|
87
391
|
// alone) at each key f is read at, twice over (a fact's own reads give more keys).
|
|
88
|
-
function withReads(facts, specs) {
|
|
392
|
+
function withReads(facts, specs, nodes = specs) {
|
|
89
393
|
const simple = facts.flatMap((f, i) => {
|
|
90
394
|
if (f.keys.length !== 1)
|
|
91
395
|
return [];
|
|
@@ -104,7 +408,7 @@ function withReads(facts, specs) {
|
|
|
104
408
|
return specs.map(n => {
|
|
105
409
|
const insts = [...n.insts], seen = new Set(insts.map(p => JSON.stringify(p)));
|
|
106
410
|
for (let round = 0; round < 2; round++) {
|
|
107
|
-
const goal = [n.hyps, n.concl, insts.map(p => substC(facts[p.fact].holds, vars(Object.fromEntries(facts[p.fact].keys.map((key, i) => [key, p.at[i]]))))), n.uses.map(u => substC(
|
|
411
|
+
const goal = [n.hyps, n.concl, insts.map(p => substC(facts[p.fact].holds, vars(Object.fromEntries(facts[p.fact].keys.map((key, i) => [key, p.at[i]]))))), n.uses.map(u => substC(nodes[u.node].concl, vars(u.subst)))];
|
|
108
412
|
for (const s of simple)
|
|
109
413
|
for (const key of readsIn(goal, s.map)) {
|
|
110
414
|
const p = { fact: s.fact, at: [key] }, id = JSON.stringify(p);
|
|
@@ -197,6 +501,14 @@ export function machineNodes(m, laws, instances) {
|
|
|
197
501
|
{ name: `${m.name}: params are naturals`, keys: [], holds: and(yes, ...m.params.map(x => le(k(0), v(x)))) },
|
|
198
502
|
];
|
|
199
503
|
const fi = (name) => facts.findIndex(f => f.name === name);
|
|
504
|
+
// The run's facts at time t: the start and the step (Bridge.lean's cycCanon has them so).
|
|
505
|
+
const run = (when) => [{ fact: 0, at: [when] }, { fact: 1, at: [when] }];
|
|
506
|
+
const nexts = new Map(state.flatMap(s => { const e = m.step.state?.[s]; return e && !(e.op === 'var' && e.name === s) && afterOn() ? [[JSON.stringify(inlineE(e)), get(`${s}@`, t)]] : []; }));
|
|
507
|
+
const afterStep = (e) => {
|
|
508
|
+
const walk = (x) => (Array.isArray(x) ? x.map(walk) : !x || typeof x !== 'object' ? x
|
|
509
|
+
: nexts.get(JSON.stringify(x)) ?? Object.fromEntries(Object.entries(x).map(([key, val]) => [key, walk(val)])));
|
|
510
|
+
return nexts.size ? walk(e) : e;
|
|
511
|
+
};
|
|
200
512
|
const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
|
|
201
513
|
// A map read at the node's time t at key e: that map's start, step and natural facts at (t, e).
|
|
202
514
|
const mapInsts = (c) => {
|
|
@@ -221,12 +533,86 @@ export function machineNodes(m, laws, instances) {
|
|
|
221
533
|
walk(c);
|
|
222
534
|
return out;
|
|
223
535
|
};
|
|
536
|
+
// Each law twice (GDP_CYCLIC_STEP=0 states the first alone, its goal the whole step): at time t, rank (t, 1), and after
|
|
537
|
+
// a step from t − 1, rank (t, 0), the law read at the state the step gives (the usual rule's step goal, over the run's
|
|
538
|
+
// cells at t − 1). The law at t is the start's at t = 0, and after t's step else: the step's equations carry the law
|
|
539
|
+
// after the step to the state at t. The law after the step uses every law at t − 1, at the keys the machine's proof
|
|
540
|
+
// assumes them at, with the functions' facts at the keys it names, as the usual rule's goal does.
|
|
541
|
+
function stepNodes(single) {
|
|
542
|
+
const L = laws.length, pre = at(less1(t)), one = le(k(1), t);
|
|
543
|
+
const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
|
|
544
|
+
const over = (e, frame) => substE(substE(e, vars(fresh)), frame);
|
|
545
|
+
// The state the step gives, over the frame before it.
|
|
546
|
+
const upd = { flags: {}, vars: Object.fromEntries(state.map(st => [st, inlineE(m.step.state?.[st] ?? v(st))])), maps: Object.fromEntries(mapNames.map(map => [map, (kk) => {
|
|
547
|
+
const w = m.step.maps?.[map];
|
|
548
|
+
// (a write's reads at its key read at the key read instead, as the map's step fact has them: the same cells there)
|
|
549
|
+
return !w ? get(map, kk) : 'each' in w ? substE(inlineE(w.value), vars({ [w.each]: kk })) : { op: 'if', c: eq(kk, inlineE(w.key)), then: readsAt(inlineE(w.value), inlineE(w.key), kk), else: get(map, kk) };
|
|
550
|
+
}])) };
|
|
551
|
+
const omegaHyp = mapNames.length ? [{ op: '<', a: T, b: omega }] : [];
|
|
552
|
+
const fnInsts = (inst, frame) => (m.facts ?? []).flatMap(f => {
|
|
553
|
+
const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
|
|
554
|
+
if (!ks.length)
|
|
555
|
+
return [{ fact, at: [] }];
|
|
556
|
+
return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ fact, at: flat.slice(i * ks.length, (i + 1) * ks.length).map(e => over(e, frame)) }));
|
|
557
|
+
});
|
|
558
|
+
const naturals = (when) => state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [when] }));
|
|
559
|
+
const params = { fact: fi(`${m.name}: params are naturals`), at: [] };
|
|
560
|
+
// The law at t first with the start's and the step's facts alone, then with the functions' facts at the keys the
|
|
561
|
+
// machine's proof names (read at t: the start's case), then as one node whose goal is the whole step (its uses the
|
|
562
|
+
// laws at t − 1), for a law after the step that isn't proven (that node then holds outright, a weak variant).
|
|
563
|
+
const nowNodes = laws.map((l, i) => {
|
|
564
|
+
const concl = substC(l.holds, at(t)), inst = instances[l.name] ?? {};
|
|
565
|
+
const hyps = [...(m.pre ? [m.pre] : []), le(t, T), ...omegaHyp], uses = [{ node: L + i, subst: {}, guard: one }];
|
|
566
|
+
const lean = [...run(t), ...mapInsts(concl), ...naturals(t), params];
|
|
567
|
+
const fns = fnInsts(inst, at(t));
|
|
568
|
+
return { name: `${m.name}: ${l.name}`, hyps, concl, rank: [t, k(1)], insts: lean, uses,
|
|
569
|
+
variants: [...(fns.length ? [{ name: `${m.name}: ${l.name}`, hyps, concl, rank: [t, k(1)], insts: [...lean, ...fns], uses }] : []), { ...single[i], rank: [t, k(1)] }] };
|
|
570
|
+
});
|
|
571
|
+
const stepNodes = laws.map((l) => {
|
|
572
|
+
const inst = instances[l.name] ?? {}, concl = substC(substC(substC(l.holds, vars(fresh)), upd), pre);
|
|
573
|
+
const uses = laws.flatMap((l2, j) => {
|
|
574
|
+
const ks = keysOf(l2);
|
|
575
|
+
if (!ks.length)
|
|
576
|
+
return [{ node: j, subst: { 't$': less1(t) }, guard: one }];
|
|
577
|
+
const flat = inst[l2.name] ?? [];
|
|
578
|
+
return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, q) => ({ node: j, subst: { 't$': less1(t), ...Object.fromEntries(ks.map((kn, r) => [kn, over(flat[q * ks.length + r], pre)])) }, guard: one }));
|
|
579
|
+
});
|
|
580
|
+
// (the request the step assumes a hypothesis of its own, which the law at t has from the step's fact: the step's
|
|
581
|
+
// equations are no hypothesis of this goal)
|
|
582
|
+
const hyps = [...(m.pre ? [m.pre] : []), one, le(t, T), ...omegaHyp, ...(m.assume ? [now(inline(m.assume), less1(t))] : [])], name = `${m.name}: ${l.name}, after a step`;
|
|
583
|
+
return { name, hyps, concl, rank: [t, k(0)], insts: [...fnInsts(inst, pre), ...naturals(less1(t)), params], uses,
|
|
584
|
+
variants: [{ name, hyps, concl: yes, rank: [t, k(0)], insts: [], uses: [], weak: true }] };
|
|
585
|
+
});
|
|
586
|
+
// The law at t reads the law after the step and the step's facts at t: the terms they share (the state the step
|
|
587
|
+
// gives) are opaque to its search, so that it carries the one to the other without splitting the step's cases.
|
|
588
|
+
for (const [i, a] of nowNodes.entries()) {
|
|
589
|
+
const o = shared(facts, a, stepNodes[i].concl);
|
|
590
|
+
if (o.length)
|
|
591
|
+
a.opaque = o;
|
|
592
|
+
}
|
|
593
|
+
return [...nowNodes, ...stepNodes];
|
|
594
|
+
}
|
|
224
595
|
const specs = laws.map(l => {
|
|
225
596
|
const inst = instances[l.name] ?? {};
|
|
226
597
|
// Terms over the frame before the step (the machine's proof proves a law about every key at a fresh key k#: here
|
|
227
598
|
// the node's own k).
|
|
228
599
|
const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
|
|
229
600
|
const pre = at(less1(t)), before = (e) => substE(substE(e, vars(fresh)), pre);
|
|
601
|
+
// A function's fact also at its keys with the terms the step gives a state variable (the usual proof reads the state
|
|
602
|
+
// after the step so) as that variable's cell at t, where it then reads the function where the law at t does: at the
|
|
603
|
+
// key its own reads are at, not one the prover would have to prove equal to it.
|
|
604
|
+
const fnReads = (x) => { const out = []; const walk = (y) => { if (y && typeof y === 'object') {
|
|
605
|
+
const o = y;
|
|
606
|
+
if (o.op === 'get' && !o.map.endsWith('@'))
|
|
607
|
+
out.push(JSON.stringify(o));
|
|
608
|
+
Object.values(o).forEach(walk);
|
|
609
|
+
} }; walk(x); return out; };
|
|
610
|
+
const concl = substC(l.holds, at(t)), reads = new Set(fnReads(concl));
|
|
611
|
+
const after = (e) => substE(afterStep(substE(e, vars(fresh))), pre);
|
|
612
|
+
const both = (tuple, make, read) => {
|
|
613
|
+
const a = make(tuple.map(before)), b = make(tuple.map(after));
|
|
614
|
+
return JSON.stringify(a) === JSON.stringify(b) || !fnReads(read(b)).some(c => reads.has(c)) ? [a] : [a, b];
|
|
615
|
+
};
|
|
230
616
|
// The laws at the step before, at the keys the machine's proof assumes them at.
|
|
231
617
|
const uses = laws.flatMap((l2, j) => {
|
|
232
618
|
const ks = keysOf(l2);
|
|
@@ -237,20 +623,58 @@ export function machineNodes(m, laws, instances) {
|
|
|
237
623
|
});
|
|
238
624
|
// The functions' facts at the keys the proof names, over the frame before the step (at t = 0, the start's).
|
|
239
625
|
const factInsts = (m.facts ?? []).flatMap(f => {
|
|
240
|
-
const ks = keysOf(f), flat = inst[f.name] ?? [];
|
|
626
|
+
const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
|
|
241
627
|
if (!ks.length)
|
|
242
|
-
return [{ fact
|
|
243
|
-
return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => (
|
|
628
|
+
return [{ fact, at: [] }];
|
|
629
|
+
return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => both(flat.slice(i * ks.length, (i + 1) * ks.length), at => ({ fact, at }), p => substC(f.holds, vars(Object.fromEntries(ks.map((kn, q) => [kn, p.at[q]])))))).flat();
|
|
244
630
|
});
|
|
245
|
-
const concl = substC(l.holds, at(t));
|
|
246
631
|
const useConcls = uses.map(us => substC(substC(laws[us.node].holds, at(t)), vars(us.subst)));
|
|
247
632
|
const base = mapInsts([concl, useConcls]);
|
|
248
633
|
return {
|
|
249
634
|
name: `${m.name}: ${l.name}`, hyps: [...(m.pre ? [m.pre] : []), le(t, T), ...(mapNames.length ? [{ op: '<', a: T, b: omega }] : [])], concl, rank: [t],
|
|
250
|
-
insts: [
|
|
635
|
+
insts: [...run(t), ...factInsts, ...base, ...state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [t] })), { fact: fi(`${m.name}: params are naturals`), at: [] }], uses,
|
|
251
636
|
};
|
|
252
637
|
});
|
|
253
|
-
return { facts, specs };
|
|
638
|
+
return { facts, specs: sliced(stepNodesOn() ? stepNodes(specs) : specs) };
|
|
639
|
+
}
|
|
640
|
+
// The terms (not a read, a variable or a constant) of a conclusion that a node's facts read too, largest first; none
|
|
641
|
+
// inside a product (a product of a term's linear form is no linear form).
|
|
642
|
+
function shared(facts, a, concl) {
|
|
643
|
+
const inst = instHyps(facts, a.insts), theirs = JSON.stringify(inst), seen = new Set(), out = [], factors = new Set();
|
|
644
|
+
const inProducts = (x, under = false) => {
|
|
645
|
+
if (!x || typeof x !== 'object')
|
|
646
|
+
return;
|
|
647
|
+
if (Array.isArray(x)) {
|
|
648
|
+
x.forEach(y => inProducts(y, under));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const o = x;
|
|
652
|
+
if (under)
|
|
653
|
+
factors.add(JSON.stringify(o));
|
|
654
|
+
for (const val of Object.values(o))
|
|
655
|
+
inProducts(val, under || o.op === 'prod');
|
|
656
|
+
};
|
|
657
|
+
inProducts([concl, inst]);
|
|
658
|
+
const walk = (x) => {
|
|
659
|
+
if (!x || typeof x !== 'object')
|
|
660
|
+
return;
|
|
661
|
+
if (Array.isArray(x)) {
|
|
662
|
+
x.forEach(walk);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const o = x, id = JSON.stringify(o);
|
|
666
|
+
if (['if', 'add', 'sub', 'min', 'max', 'mul', 'prod'].includes(o.op) && theirs.includes(id) && !factors.has(id)) {
|
|
667
|
+
if (!seen.has(id)) {
|
|
668
|
+
seen.add(id);
|
|
669
|
+
out.push(o);
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
for (const val of Object.values(o))
|
|
674
|
+
walk(val);
|
|
675
|
+
};
|
|
676
|
+
walk(concl);
|
|
677
|
+
return out.sort((x, y) => JSON.stringify(y).length - JSON.stringify(x).length);
|
|
254
678
|
}
|
|
255
679
|
// --- folds over trees -----------------------------------------------------------------------------------------------
|
|
256
680
|
// The tree as a table of nodes below its size N: a node n is a leaf (Kind[n] = 0) whose values come from its inputs,
|
|
@@ -271,11 +695,29 @@ export function foldNodes(f, laws) {
|
|
|
271
695
|
insts: [{ fact: 0, at: [n] }, ...[n, left(n), right(n)].map(i => ({ fact: 1, at: [i] })), { fact: 2, at: [] }],
|
|
272
696
|
uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: eq(kind(n), k(1)) }))),
|
|
273
697
|
}));
|
|
274
|
-
|
|
698
|
+
if (!stepNodesOn())
|
|
699
|
+
return { facts, specs: sliced(specs) };
|
|
700
|
+
// Each law twice, as a machine's are (GDP_CYCLIC_STEP=0 states the first alone): at a node n, rank (n, 1), from the
|
|
701
|
+
// table's leaf case or from the law at a node from its children, rank (n, 0), the law read at the values the node's
|
|
702
|
+
// case gives (the usual rule's node goal, over the table's cells at its children), using every law at both children.
|
|
703
|
+
const L = laws.length, hyps = [...(f.pre ? [f.pre] : []), { op: '<', a: n, b: N }], inner = eq(kind(n), k(1));
|
|
704
|
+
const valuesAt = vars({ ...Object.fromEntries(f.inputs.map(x => [x, get(`${x}@`, n)])), ...Object.fromEntries(names.map(x => [x, substE(f.node[x], nodeAt(n))])) });
|
|
705
|
+
const fromChildren = laws.map((l) => {
|
|
706
|
+
const name = `${f.name}: ${l.name}, from its children`, h = [...hyps, inner];
|
|
707
|
+
return { name, hyps: h, concl: substC(l.holds, valuesAt), rank: [n, k(0)], insts: [{ fact: 0, at: [n] }, ...[left(n), right(n)].map(i => ({ fact: 1, at: [i] })), { fact: 2, at: [] }],
|
|
708
|
+
uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: yes }))), variants: [{ name, hyps: h, concl: yes, rank: [n, k(0)], insts: [], uses: [], weak: true }] };
|
|
709
|
+
});
|
|
710
|
+
const atNode = laws.map((l, i) => {
|
|
711
|
+
const a = { name: `${f.name}: ${l.name}`, hyps, concl: substC(l.holds, at(n)), rank: [n, k(1)], insts: [{ fact: 0, at: [n] }, { fact: 1, at: [n] }, { fact: 2, at: [] }],
|
|
712
|
+
uses: [{ node: L + i, subst: {}, guard: inner }], variants: [{ ...specs[i], rank: [n, k(1)] }] };
|
|
713
|
+
const o = shared(facts, a, fromChildren[i].concl);
|
|
714
|
+
return o.length ? { ...a, opaque: o } : a;
|
|
715
|
+
});
|
|
716
|
+
return { facts, specs: sliced([...atNode, ...fromChildren]) };
|
|
275
717
|
}
|
|
276
718
|
// --- the cyclic route beside the usual one (GDP_CYCLIC=1) -------------------------------------------------------------
|
|
277
719
|
// With GDP_CYCLIC=1, every proof the instances above cover is made again as a cyclic proof, and a verdict that differs
|
|
278
|
-
// throws: the suite run so shows the rules are instances. Each comparison is printed to stderr as a line `cyclic: {...}`.
|
|
720
|
+
// throws (GDP_CYCLIC_LAX=1 only prints it, to measure a whole run): the suite run so shows the rules are instances. Each comparison is printed to stderr as a line `cyclic: {...}`.
|
|
279
721
|
const env = () => globalThis.process?.env;
|
|
280
722
|
let quiet = 0;
|
|
281
723
|
export const cyclicOn = () => quiet === 0 && env()?.GDP_CYCLIC === '1';
|
|
@@ -287,9 +729,12 @@ finally {
|
|
|
287
729
|
quiet--;
|
|
288
730
|
} };
|
|
289
731
|
export function crossCyclic(kind, name, usual, run, usualMs) {
|
|
732
|
+
for (const key of Object.keys(cyclicProf))
|
|
733
|
+
delete cyclicProf[key];
|
|
290
734
|
const t0 = performance.now(), out = run(), ms = Math.round(performance.now() - t0);
|
|
291
735
|
const cyclic = out.proven && out.checked;
|
|
292
|
-
|
|
293
|
-
|
|
736
|
+
const prof = profOn() ? { prof: Object.fromEntries(Object.entries(cyclicProf).map(([key, p]) => [key, Object.fromEntries(Object.entries(p).map(([f, x]) => [f, Math.round(x)]))])) } : {};
|
|
737
|
+
console.error(`cyclic: ${JSON.stringify({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), ...prof, ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) })}`);
|
|
738
|
+
if (cyclic !== usual && env()?.GDP_CYCLIC_LAX !== '1')
|
|
294
739
|
throw new Error(`cyclic route: ${kind} ${name} is ${cyclic ? 'proven' : 'not proven'} as a cyclic proof, ${usual ? 'proven' : 'not proven'} the usual way${out.proven ? (out.why ? ` (${out.why})` : '') : ` (${out.goal}: ${out.stuck})`}`);
|
|
295
740
|
}
|