gdp-wired 0.16.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.
@@ -0,0 +1,740 @@
1
+ import { substC, substE } from "../kernel/kernel.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
+ const v = (name) => ({ op: 'var', name });
5
+ const k = (value) => ({ op: 'const', value });
6
+ const add = (a, b) => ({ op: 'add', a, b });
7
+ const less1 = (a) => ({ op: 'sub', a, b: k(1) });
8
+ const get = (map, key) => ({ op: 'get', map, key });
9
+ const le = (a, b) => ({ op: '<=', a, b });
10
+ const eq = (a, b) => ({ op: '=', a, b });
11
+ const and = (...xs) => (xs.length === 1 ? xs[0] : { op: 'and', xs });
12
+ const implies = (a, b) => ({ op: 'or', xs: [{ op: 'not', x: a }, b] });
13
+ const yes = le(k(0), k(0));
14
+ const vars = (s) => ({ vars: s, maps: {}, flags: {} });
15
+ // A map's cell at time u and key k, in the run's one map over both: u + k·Ω, where Ω is above the run's length T, so
16
+ // that no two (u, k) with u <= T meet (u < Ω). Linear in u, and two keys' cells differ by (k − k')·Ω.
17
+ const omega = v('O$');
18
+ export const pair = (u, key) => add(u, { op: 'prod', a: key, b: omega });
19
+ // Every goal grown, then the whole checked by the kernel.
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';
159
+ cellProducts.on = true;
160
+ try {
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 };
177
+ }
178
+ finally {
179
+ cellProducts.on = was;
180
+ }
181
+ };
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;
318
+ }
319
+ nodes[i] = r;
320
+ shapes[i] = spec;
321
+ failed = undefined;
322
+ if (spec.weak)
323
+ weak.add(i);
324
+ break;
325
+ }
326
+ if (failed || !nodes[i])
327
+ return failed ?? { proven: false, checked: false, node: specs[i].name, goal: specs[i].name, stuck: 'unknown' };
328
+ }
329
+ const proof = { facts, nodes }, why = timedKind('check', 0, () => refuseCyclic(proof));
330
+ return { proven: true, checked: why === undefined, proof, ...(why ? { why } : {}) };
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 })) } : {}) }));
334
+ // The facts of a machine's functions (laws about every key of them), as cyclic facts.
335
+ const factOf = (l) => ({ name: l.name, keys: [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])], holds: l.holds });
336
+ export function recurNodes(r) {
337
+ const f = `${r.name}$`, x = v('x'), y = v('y'), kk = v('k$');
338
+ const facts = [
339
+ { name: `${r.name}(0)`, keys: [], holds: eq(get(f, k(0)), r.zero) },
340
+ { name: `${r.name}(x + 1)`, keys: ['k$'], holds: eq(get(f, add(kk, k(1))), substE(r.next, vars({ x: kk, prev: get(f, kk) }))) },
341
+ ...r.facts.map(factOf),
342
+ ];
343
+ const at = (t) => vars({ x: t, fx: get(f, t) });
344
+ const n = r.lemmas.length;
345
+ const lemmaUses = (to, guard) => r.lemmas.map((_, j) => ({ node: j, subst: { x: to }, guard }));
346
+ const specs = r.lemmas.map(l => ({
347
+ name: `${r.name}: ${l.name}`, hyps: [], concl: substC(l.holds, at(x)), rank: [k(0), x],
348
+ insts: [{ fact: 0, at: [] }, { fact: 1, at: [less1(x)] }], uses: lemmaUses(less1(x), le(k(1), x)),
349
+ }));
350
+ // A law between two points: for every x <= y, by induction on y from x, the lemmas at x, at y and at y − 1.
351
+ for (const b of r.between)
352
+ specs.push({
353
+ name: `${r.name}: ${b.name}`, hyps: [], rank: [k(1), y],
354
+ concl: implies(le(x, y), substC(b.holds, vars({ x, fx: get(f, x), y, fy: get(f, y) }))),
355
+ insts: [{ fact: 1, at: [less1(y)] }],
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))],
357
+ });
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))) };
389
+ }
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
391
+ // alone) at each key f is read at, twice over (a fact's own reads give more keys).
392
+ function withReads(facts, specs, nodes = specs) {
393
+ const simple = facts.flatMap((f, i) => {
394
+ if (f.keys.length !== 1)
395
+ return [];
396
+ const reads = [...JSON.stringify(f.holds).matchAll(/"op":"get","map":"([^"]+)","key":\{"op":"var","name":"([^"]+)"\}/g)].filter(m => m[2] === f.keys[0]);
397
+ return reads.map(m => ({ fact: i, map: m[1] }));
398
+ });
399
+ if (!simple.length)
400
+ return specs;
401
+ const readsIn = (t, map) => { const out = []; const walk = (x) => { if (x && typeof x === 'object') {
402
+ const o = x;
403
+ if (o.op === 'get' && o.map === map)
404
+ out.push(o.key);
405
+ for (const val of Object.values(o))
406
+ walk(val);
407
+ } }; walk(t); return out; };
408
+ return specs.map(n => {
409
+ const insts = [...n.insts], seen = new Set(insts.map(p => JSON.stringify(p)));
410
+ for (let round = 0; round < 2; round++) {
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)))];
412
+ for (const s of simple)
413
+ for (const key of readsIn(goal, s.map)) {
414
+ const p = { fact: s.fact, at: [key] }, id = JSON.stringify(p);
415
+ if (!seen.has(id)) {
416
+ seen.add(id);
417
+ insts.push(p);
418
+ }
419
+ }
420
+ }
421
+ return { ...n, insts };
422
+ });
423
+ }
424
+ // --- machines -------------------------------------------------------------------------------------------------------
425
+ // The machines this instance covers: no history, no flag holding at every key, and no law that reads a map at a key
426
+ // read from a map of the state (a table of nodes: a node's fold at its child), whose cells a run's maps put at keys
427
+ // the prover would have to prove equal through products (`machineNodes` proves those laws far slower than the usual
428
+ // rule, past its budget: research/cyclic/probe.ts).
429
+ export const cyclicMachine = (m, laws = []) => {
430
+ const maps = Object.keys(m.maps ?? {});
431
+ const nested = (t) => { let found = false; const walk = (x, inKey) => { if (found || !x || typeof x !== 'object')
432
+ return; const o = x; if (o.op === 'get' && inKey && maps.includes(o.map)) {
433
+ found = true;
434
+ return;
435
+ } if (o.op === 'get') {
436
+ walk(o.key, true);
437
+ return;
438
+ } for (const val of Object.values(o))
439
+ walk(val, inKey); }; walk(t, false); return found; };
440
+ return !m.history && !Object.values(m.step.flags ?? {}).some(c => c.op === 'all') && !laws.some(l => nested(l.holds));
441
+ };
442
+ // The run as maps over time: names at time u read as their maps there.
443
+ function trace(m) {
444
+ const maps = [...Object.keys(m.maps ?? {}), ...(m.inputMaps ?? [])];
445
+ const at = (u) => ({
446
+ vars: Object.fromEntries([...Object.keys(m.state), ...m.inputs].map(n => [n, get(`${n}@`, u)])),
447
+ maps: Object.fromEntries(maps.map(map => [map, (key) => get(`${map}@`, pair(u, key))])), flags: {},
448
+ });
449
+ return { at };
450
+ }
451
+ // Every map read at `from` (the same term) read at `to`.
452
+ const readsAt = (t, from, to) => {
453
+ const id = JSON.stringify(from);
454
+ const walk = (x) => {
455
+ if (Array.isArray(x))
456
+ return x.map(walk);
457
+ if (!x || typeof x !== 'object')
458
+ return x;
459
+ const o = x;
460
+ if (o.op === 'get' && JSON.stringify(o.key) === id)
461
+ return { ...o, key: to };
462
+ return Object.fromEntries(Object.entries(o).map(([key, val]) => [key, walk(val)]));
463
+ };
464
+ return walk(t);
465
+ };
466
+ // A machine's flags, each its condition over the frame before the step (through the flags before it).
467
+ const flagsOf = (m) => {
468
+ const out = {};
469
+ for (const [f, c] of Object.entries(m.step.flags ?? {}))
470
+ out[f] = substC(c, { vars: {}, maps: {}, flags: out });
471
+ return out;
472
+ };
473
+ export function machineNodes(m, laws, instances) {
474
+ const { at } = trace(m), flags = flagsOf(m), inline = (t) => substC(t, { vars: {}, maps: {}, flags });
475
+ const t = v('t$'), T = v('T$'), u = v('u$'), key = v('k$');
476
+ const inlineE = (e) => substE(e, { vars: {}, maps: {}, flags });
477
+ const now = (c, when) => substC(c, at(when)), nowE = (e, when) => substE(inlineE(e), at(when));
478
+ const state = Object.keys(m.state), mapNames = Object.keys(m.maps ?? {});
479
+ // The start and a step, each stated at the time u it gives (a start at u <= 0, a step at 1 <= u <= T from u − 1), so a
480
+ // node reads them at its own time t: the cells they give are the cells it reads, with no key to prove equal.
481
+ const start = le(u, k(0)), stepped = and(le(k(1), u), le(u, T)), prev = less1(u);
482
+ const facts = [
483
+ { name: `${m.name}: the start`, keys: ['u$'], holds: implies(start, and(yes, ...state.map(s => eq(get(`${s}@`, u), m.state[s])))) },
484
+ { name: `${m.name}: a step`, keys: ['u$'], holds: implies(stepped, and(yes, ...(m.assume ? [now(inline(m.assume), prev)] : []), ...state.map(s => eq(get(`${s}@`, u), nowE(m.step.state?.[s] ?? v(s), prev))))) },
485
+ ...mapNames.flatMap((map) => {
486
+ const w = m.step.maps?.[map], cell = (when) => get(`${map}@`, pair(when, key));
487
+ // A write at one key: where the key is the write's, the value with every read at the write's key read at the key
488
+ // instead (the same cell there), so the cells the step gives are at the key the node reads.
489
+ const next = !w ? cell(prev) : 'each' in w ? substE(substE(w.value, { vars: { [w.each]: key }, maps: {}, flags }), at(prev))
490
+ : { op: 'if', c: eq(key, nowE(w.key, prev)), then: substE(readsAt(inlineE(w.value), inlineE(w.key), key), at(prev)), else: cell(prev) };
491
+ return [
492
+ { name: `${m.name}: ${map} at the start`, keys: ['u$', 'k$'], holds: implies(start, eq(cell(u), k(m.maps[map]))) },
493
+ { name: `${m.name}: ${map} at a step`, keys: ['u$', 'k$'], holds: implies(stepped, eq(cell(u), next)) },
494
+ { name: `${m.name}: ${map}'s cells are naturals`, keys: ['u$', 'k$'], holds: le(k(0), cell(u)) },
495
+ ];
496
+ }),
497
+ ...(m.facts ?? []).map(factOf),
498
+ // That a state variable's value at a time is a natural holds of every model: read at the node's time, it makes the
499
+ // value a column of its own (a value read only inside a product would be none, and no two reads of it equal).
500
+ ...state.map((s) => ({ name: `${m.name}: ${s} is a natural`, keys: ['u$'], holds: le(k(0), get(`${s}@`, u)) })),
501
+ { name: `${m.name}: params are naturals`, keys: [], holds: and(yes, ...m.params.map(x => le(k(0), v(x)))) },
502
+ ];
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
+ };
512
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
513
+ // A map read at the node's time t at key e: that map's start, step and natural facts at (t, e).
514
+ const mapInsts = (c) => {
515
+ const out = [], seen = new Set();
516
+ const walk = (x) => {
517
+ if (!x || typeof x !== 'object')
518
+ return;
519
+ const o = x;
520
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && mapNames.includes(o.map.slice(0, -1))) {
521
+ const kk = o.key;
522
+ if (kk.op === 'add' && JSON.stringify(kk.a) === JSON.stringify(t) && kk.b.op === 'prod') {
523
+ const read = kk.b.a, id = JSON.stringify(read), map = o.map.slice(0, -1);
524
+ if (!seen.has(`${map}:${id}`)) {
525
+ seen.add(`${map}:${id}`);
526
+ out.push(...['at the start', 'at a step', '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [t, read] })));
527
+ }
528
+ }
529
+ }
530
+ for (const val of Object.values(o))
531
+ walk(val);
532
+ };
533
+ walk(c);
534
+ return out;
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
+ }
595
+ const specs = laws.map(l => {
596
+ const inst = instances[l.name] ?? {};
597
+ // Terms over the frame before the step (the machine's proof proves a law about every key at a fresh key k#: here
598
+ // the node's own k).
599
+ const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
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
+ };
616
+ // The laws at the step before, at the keys the machine's proof assumes them at.
617
+ const uses = laws.flatMap((l2, j) => {
618
+ const ks = keysOf(l2);
619
+ if (!ks.length)
620
+ return [{ node: j, subst: { 't$': less1(t) }, guard: le(k(1), t) }];
621
+ const flat = inst[l2.name] ?? [];
622
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ node: j, subst: { 't$': less1(t), ...Object.fromEntries(ks.map((kn, q) => [kn, before(flat[i * ks.length + q])])) }, guard: le(k(1), t) }));
623
+ });
624
+ // The functions' facts at the keys the proof names, over the frame before the step (at t = 0, the start's).
625
+ const factInsts = (m.facts ?? []).flatMap(f => {
626
+ const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
627
+ if (!ks.length)
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();
630
+ });
631
+ const useConcls = uses.map(us => substC(substC(laws[us.node].holds, at(t)), vars(us.subst)));
632
+ const base = mapInsts([concl, useConcls]);
633
+ return {
634
+ name: `${m.name}: ${l.name}`, hyps: [...(m.pre ? [m.pre] : []), le(t, T), ...(mapNames.length ? [{ op: '<', a: T, b: omega }] : [])], concl, rank: [t],
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,
636
+ };
637
+ });
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);
678
+ }
679
+ // --- folds over trees -----------------------------------------------------------------------------------------------
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,
681
+ // or a node (Kind[n] = 1) with children Left[n], Right[n] numbered below n, its values from theirs and its inputs.
682
+ export function foldNodes(f, laws) {
683
+ const n = v('n$'), N = v('N$'), names = Object.keys(f.leaf);
684
+ const at = (i) => vars(Object.fromEntries([...names, ...f.inputs].map(x => [x, get(`${x}@`, i)])));
685
+ const kind = (i) => get('Kind@', i), left = (i) => get('Left@', i), right = (i) => get('Right@', i);
686
+ const side = (child, p) => Object.fromEntries(names.map(x => [`${p}${x}`, get(`${x}@`, child)]));
687
+ const nodeAt = (i) => vars({ ...Object.fromEntries(f.inputs.map(x => [x, get(`${x}@`, i)])), ...side(left(i), 'l_'), ...side(right(i), 'r_') });
688
+ const assume = (i) => (f.assume ? [substC(f.assume, at(i))] : []);
689
+ const facts = [{ name: `${f.name}: the table`, keys: ['n$'], holds: implies({ op: '<', a: n, b: N }, and(le(kind(n), k(1)), implies(eq(kind(n), k(0)), and(yes, ...assume(n), ...names.map(x => eq(get(`${x}@`, n), substE(f.leaf[x], at(n)))))), implies(eq(kind(n), k(1)), and({ op: '<', a: left(n), b: n }, { op: '<', a: right(n), b: n }, ...assume(n), ...names.map(x => eq(get(`${x}@`, n), substE(f.node[x], nodeAt(n)))))))) },
690
+ // Each value a natural (true of every model), so a value read only inside a product is a column of its own.
691
+ { name: `${f.name}: values are naturals`, keys: ['n$'], holds: and(yes, ...names.map(x => le(k(0), get(`${x}@`, n)))) },
692
+ { name: `${f.name}: params are naturals`, keys: [], holds: and(yes, ...f.params.map(x => le(k(0), v(x)))) }];
693
+ const specs = laws.map(l => ({
694
+ name: `${f.name}: ${l.name}`, hyps: [...(f.pre ? [f.pre] : []), { op: '<', a: n, b: N }], concl: substC(l.holds, at(n)), rank: [n],
695
+ insts: [{ fact: 0, at: [n] }, ...[n, left(n), right(n)].map(i => ({ fact: 1, at: [i] })), { fact: 2, at: [] }],
696
+ uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: eq(kind(n), k(1)) }))),
697
+ }));
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]) };
717
+ }
718
+ // --- the cyclic route beside the usual one (GDP_CYCLIC=1) -------------------------------------------------------------
719
+ // With GDP_CYCLIC=1, every proof the instances above cover is made again as a cyclic proof, and a verdict that differs
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: {...}`.
721
+ const env = () => globalThis.process?.env;
722
+ let quiet = 0;
723
+ export const cyclicOn = () => quiet === 0 && env()?.GDP_CYCLIC === '1';
724
+ // A search's many trial proofs (core/discover.ts, core/infer.ts) aren't compared: what it finds is proven again.
725
+ export const quietly = (f) => { quiet++; try {
726
+ return f();
727
+ }
728
+ finally {
729
+ quiet--;
730
+ } };
731
+ export function crossCyclic(kind, name, usual, run, usualMs) {
732
+ for (const key of Object.keys(cyclicProf))
733
+ delete cyclicProf[key];
734
+ const t0 = performance.now(), out = run(), ms = Math.round(performance.now() - t0);
735
+ const cyclic = out.proven && out.checked;
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')
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})`}`);
740
+ }