gdp-wired 0.17.0 → 0.19.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.
@@ -1,6 +1,7 @@
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";
1
+ import { assumed, boundOf, foldObligations, obligations, substC, substE, witnessesOf } 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
+ import { carry, decide, readsFirst, renamer, translate, tstats } from "./translate.js";
4
5
  const v = (name) => ({ op: 'var', name });
5
6
  const k = (value) => ({ op: 'const', value });
6
7
  const add = (a, b) => ({ op: 'add', a, b });
@@ -17,49 +18,334 @@ const vars = (s) => ({ vars: s, maps: {}, flags: {} });
17
18
  const omega = v('O$');
18
19
  export const pair = (u, key) => add(u, { op: 'prod', a: key, b: omega });
19
20
  // Every goal grown, then the whole checked by the kernel.
20
- export function proveCyclic(facts, specs) {
21
- const nodes = [];
22
- const grown = (g, node) => {
23
- const was = cellProducts.on;
21
+ // GDP_CYCLIC_PROF=1: each comparison's line also has where the cyclic proof's time went, by goal kind (main, premise,
22
+ // descent, the kernel's check): ms, goals, hypotheses, solver and row ms.
23
+ export const cyclicProf = {};
24
+ const profOn = () => env()?.GDP_CYCLIC_PROF === '1';
25
+ export function timedKind(kind, hyps, f) {
26
+ if (!profOn())
27
+ return f();
28
+ const was = profile.on, s0 = profile.solveMs, r0 = profile.rowsMs, t0 = performance.now();
29
+ profile.on = true;
30
+ try {
31
+ return f();
32
+ }
33
+ finally {
34
+ profile.on = was;
35
+ const p = (cyclicProf[kind] ??= { ms: 0, n: 0, hyps: 0, solveMs: 0, rowsMs: 0 });
36
+ p.ms += performance.now() - t0;
37
+ p.n++;
38
+ p.hyps += hyps;
39
+ p.solveMs += profile.solveMs - s0;
40
+ p.rowsMs += profile.rowsMs - r0;
41
+ }
42
+ }
43
+ // Each goal grown first on what it reads: its node's hypotheses and its own last ones (a use's guard, the conclusion
44
+ // negated) kept whole, and of the rest (the facts at the node's terms, the uses) only the parts it reaches: a
45
+ // conjunction's conjuncts (under a guard, as a step's equations are, one per state variable) each a part of its own; an
46
+ // 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
47
+ // its terms' cells and its guard's; any other part reached when it shares a cell with what is, or reads none. The
48
+ // weaker goal's tree is the whole goal's (`widen`); where the weaker goal isn't refuted, the whole is grown as before.
49
+ // GDP_CYCLIC_SLICE=0 grows the whole goal only.
50
+ // 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
51
+ // 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.
52
+ const cellsIn = (t, out = new Set()) => {
53
+ if (t && typeof t === 'object') {
54
+ const o = t, map = o.map;
55
+ if (o.op === 'get') {
56
+ const key = o.key;
57
+ 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);
58
+ }
59
+ for (const val of Object.values(o))
60
+ cellsIn(val, out);
61
+ }
62
+ return out;
63
+ };
64
+ function partsOf(c, guards = [], out = []) {
65
+ if (c.op === 'and')
66
+ c.xs.forEach(x => partsOf(x, guards, out));
67
+ else if (c.op === 'or' && c.xs.length === 2 && c.xs[0].op === 'not')
68
+ partsOf(c.xs[1], [...guards, c.xs[0]], out);
69
+ else
70
+ out.push({ c, guards });
71
+ return out;
72
+ }
73
+ function rebuild(c, keep, at = { j: 0 }) {
74
+ if (c.op === 'and') {
75
+ const xs = c.xs.flatMap(x => { const y = rebuild(x, keep, at); return y ? [y] : []; });
76
+ return xs.length ? and(...xs) : undefined;
77
+ }
78
+ if (c.op === 'or' && c.xs.length === 2 && c.xs[0].op === 'not') {
79
+ const b = rebuild(c.xs[1], keep, at);
80
+ return b && { op: 'or', xs: [c.xs[0], b] };
81
+ }
82
+ return keep(at.j++) ? c : undefined;
83
+ }
84
+ // The hypotheses with a part the goal reaches (whole ones included).
85
+ export function reached(g, whole) { return slice(g, whole, true); }
86
+ export function slice(g, whole, hypsOnly = false) {
87
+ const reach = new Set(), parts = g.hyps.map(h => partsOf(h));
88
+ const pieces = [];
89
+ g.hyps.forEach((h, i) => {
90
+ if (whole(i)) {
91
+ cellsIn(h, reach);
92
+ return;
93
+ }
94
+ parts[i].forEach(({ c, guards }, j) => {
95
+ // (an equation between two reads, a law's, gives neither: it is reached by either)
96
+ 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);
97
+ for (const x of guards)
98
+ cellsIn(x, cells);
99
+ pieces.push({ c, guards, i, j, gives, cells });
100
+ });
101
+ });
102
+ const kept = new Set();
103
+ for (let grew = true; grew;) {
104
+ grew = false;
105
+ for (const [q, p] of pieces.entries()) {
106
+ if (kept.has(q) || (p.gives ? !reach.has(p.gives) : p.cells.size && ![...p.cells].some(c => reach.has(c))))
107
+ continue;
108
+ kept.add(q);
109
+ grew = true;
110
+ for (const c of p.cells)
111
+ reach.add(c);
112
+ if (p.gives)
113
+ reach.add(p.gives);
114
+ }
115
+ }
116
+ if (hypsOnly)
117
+ return new Set(g.hyps.flatMap((_, i) => (whole(i) || pieces.some((p, q) => p.i === i && kept.has(q)) ? [i] : [])));
118
+ // The equations that give a cell the conclusion reads (a step's, the start's) join the conclusion, under their guards:
119
+ // the prover splits the conclusion's atoms first, as the usual rule does a law read after the step.
120
+ const last = g.hyps.length - 1, concl = whole(last) ? cellsIn(g.hyps[last]) : new Set();
121
+ const joins = (p) => !!p.gives && concl.has(p.gives);
122
+ if (kept.size === pieces.length && !pieces.some(joins))
123
+ return undefined;
124
+ const stays = new Set(), joined = [];
125
+ for (const [q, p] of pieces.entries())
126
+ if (kept.has(q)) {
127
+ if (joins(p))
128
+ joined.push(p.guards.reduceRight((acc, x) => ({ op: 'or', xs: [x, acc] }), p.c));
129
+ else
130
+ stays.add(`${p.i}:${p.j}`);
131
+ }
132
+ const from = [], joinedFrom = [...new Set(pieces.flatMap((p, q) => (kept.has(q) && joins(p) ? [p.i] : [])))];
133
+ const hyps = g.hyps.flatMap((h, i) => {
134
+ const r = whole(i) || parts[i].every((_, j) => stays.has(`${i}:${j}`)) ? h : rebuild(h, j => stays.has(`${i}:${j}`));
135
+ if (r)
136
+ from.push([i]);
137
+ return r ? [r] : [];
138
+ });
139
+ if (joined.length) {
140
+ hyps[hyps.length - 1] = and(hyps.at(-1), ...joined);
141
+ from[from.length - 1].push(...joinedFrom);
142
+ }
143
+ return { name: g.name, hyps, from };
144
+ }
145
+ const withProducts = (f) => { const was = cellProducts.on; cellProducts.on = true; try {
146
+ return f();
147
+ }
148
+ finally {
149
+ cellProducts.on = was;
150
+ } };
151
+ let sliceNow = false;
152
+ const sliceOn = () => sliceNow && env()?.GDP_CYCLIC_SLICE !== '0';
153
+ const afterOn = () => env()?.GDP_CYCLIC_AFTER !== '0';
154
+ const stepNodesOn = () => env()?.GDP_CYCLIC_STEP !== '0';
155
+ const searched = new Map();
156
+ export const translatedCache = new Map();
157
+ // `order`: the order the nodes are grown in, by default those with a weak variant first (a variant that uses a weak node
158
+ // is passed over).
159
+ // `made`: the first nodes already made (a proof of the same facts whose nodes these specs begin with), taken as they are.
160
+ export function proveCyclic(facts, specs, order0, grownBefore = searched, st = tstats(), made = []) {
161
+ const order = (order0 ?? [...specs.keys()].sort((i, j) => Number(!specs[i].variants?.some(x => x.weak)) - Number(!specs[j].variants?.some(x => x.weak)))).filter(i => i >= made.length);
162
+ const nodes = [...made], shapes = [...specs], weak = new Set();
163
+ let later, refuted = false;
164
+ // A goal's tree, and the hypotheses its leaves read (of the whole goal, a weaker goal's read through where they came from).
165
+ const grown = (g, node, own) => {
166
+ const was = cellProducts.on, kind = g.name.endsWith('its rank below') ? 'descent' : g.name.includes(': use of node') ? 'premise' : 'main';
24
167
  cellProducts.on = true;
25
- let o;
26
168
  try {
27
- o = grow(g);
169
+ const sliced = sliceOn() ? slice(g, own) : undefined, part = sliced && hide(sliced);
170
+ if (part) {
171
+ // (within two seconds: a weaker goal no easier than the whole is left to the whole)
172
+ 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) }));
173
+ if (env()?.GDP_CYCLIC_DEBUG)
174
+ 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`);
175
+ if ('tree' in p)
176
+ return { tree: p.tree, from: part, used: () => new Set([...usedHyps(part, p.tree)].flatMap(i => part.from[i])) };
177
+ if (part !== sliced) {
178
+ const q = timedKind(`${kind} sliced`, sliced.hyps.length, () => grow(sliced, new Map(), { nodes: 0, until: performance.now() + Math.min(2_000, later ?? 2_000) }));
179
+ if ('tree' in q)
180
+ return { tree: q.tree, from: sliced, used: () => new Set([...usedHyps(sliced, q.tree)].flatMap(i => sliced.from[i])) };
181
+ }
182
+ }
183
+ 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)) }));
184
+ 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
185
  }
29
186
  finally {
30
187
  cellProducts.on = was;
31
188
  }
32
- return 'tree' in o ? o.tree : { proven: false, checked: false, node, goal: g.name, stuck: o.stuck };
33
189
  };
34
- for (const n of specs) {
35
- const main = grown(mainGoal(facts, specs, n), n.name);
36
- if ('proven' in main)
37
- return main;
38
- const uses = [];
39
- for (const u of n.uses) {
40
- const m = specs[u.node];
41
- if (!m)
42
- return { proven: false, checked: false, node: n.name, goal: `use of node ${u.node}`, stuck: 'no such node' };
43
- const premises = [];
44
- for (const [i, c] of m.hyps.entries()) {
45
- const t = grown(premiseGoal(facts, n, u, c, i), n.name);
46
- if ('proven' in t)
47
- return t;
48
- premises.push(t);
190
+ // The tree made the goal's own (a weaker goal's widened), or grown on the goal itself where that fails.
191
+ const own = (r, g) => (r.from === g ? r.tree : timedKind('widen', 0, () => widen(r.from, g, r.tree, opaqueOf.get(r.from))));
192
+ const whole = (g, node, mine) => {
193
+ const key = JSON.stringify(g.hyps), before = grownBefore.get(key);
194
+ if (before)
195
+ return before.tree;
196
+ const r = grown(g, node, mine);
197
+ if ('proven' in r)
198
+ return r;
199
+ const t = own(r, g) ?? (() => { const o = grow(g); return 'tree' in o ? o.tree : undefined; })();
200
+ if (!t)
201
+ return { proven: false, checked: false, node, goal: g.name, stuck: 'unknown' };
202
+ if (grownBefore.size >= 4096)
203
+ grownBefore.clear();
204
+ grownBefore.set(key, { tree: t });
205
+ return t;
206
+ };
207
+ // A goal with the node's opaque terms read as variables (z$0, z$1, ...), remembered with them for `widen`.
208
+ const opaqueOf = new WeakMap();
209
+ let hidden = [];
210
+ const hide = (g) => {
211
+ if (!hidden.length)
212
+ return g;
213
+ let text = JSON.stringify(g.hyps);
214
+ const names = {};
215
+ hidden.forEach((e, i) => { const id = JSON.stringify(e); if (text.includes(id)) {
216
+ text = text.replaceAll(id, JSON.stringify(v(`z$${i}`)));
217
+ names[`z$${i}`] = e;
218
+ } });
219
+ if (!Object.keys(names).length)
220
+ return g;
221
+ const out = { ...g, hyps: JSON.parse(text) };
222
+ opaqueOf.set(out, names);
223
+ return out;
224
+ };
225
+ // A node's goal grown; then the node keeps only the facts and uses its tree reads (the fewer, the fewer hypotheses its
226
+ // goal has for the kernel to read at every leaf, and the fewer uses to prove below); then each use's goals.
227
+ const attempt = ({ opaque, variants: _v, weak: _w, slice: sl, main: given0, carry: made, ...n0 }) => {
228
+ hidden = opaque ?? [];
229
+ sliceNow = !!sl;
230
+ // A goal's own hypotheses: the node's, and those after its facts and uses.
231
+ const n1 = { ...n0, insts: n0.insts.filter(p => facts[p.fact]) };
232
+ const given = given0 ?? (made ? timedKind('carry', 0, () => withProducts(() => carry(mainGoal(facts, shapes, n1), n1.insts.length, n1.uses.length, st, performance.now() + 2_000))) : undefined);
233
+ if (made && !given0) {
234
+ if (given)
235
+ st.carried++;
236
+ else
237
+ st.uncarried++;
238
+ }
239
+ const base = baseHyps(facts, n1).length, mine = (tail) => (i) => i < n1.hyps.length || i >= base + tail;
240
+ const g = given ? undefined : mainGoal(facts, shapes, n1), key = g ? JSON.stringify(g.hyps) : '', before = g && grownBefore.get(key);
241
+ let n = n1, main = given, src;
242
+ if (!g) { /* the tree given */ }
243
+ else if (before) {
244
+ n = { ...n1, insts: before.insts?.map(i => n1.insts[i]) ?? n1.insts, uses: before.uses?.map(j => n1.uses[j]) ?? n1.uses };
245
+ main = before.tree;
246
+ src = { tree: main, from: mainGoal(facts, shapes, n) };
247
+ }
248
+ else {
249
+ // First the node with only the facts and uses its goal reaches, its goal grown as it is (within two seconds).
250
+ const h0 = n1.hyps.length, keep = sliceOn() ? reached(g, mine(n1.uses.length)) : undefined;
251
+ const n2 = keep && { ...n1, insts: n1.insts.filter((_, i) => keep.has(h0 + i)), uses: n1.uses.filter((_, j) => keep.has(base + j)) };
252
+ if (n2 && (n2.insts.length < n1.insts.length || n2.uses.length < n1.uses.length)) {
253
+ const g2 = mainGoal(facts, shapes, n2), part = hide(g2);
254
+ 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) })));
255
+ if (env()?.GDP_CYCLIC_DEBUG)
256
+ console.error(`[cyc] ${g.name}: ${g2.hyps.length} of ${g.hyps.length} hyps selected, ${'tree' in p ? `${p.nodes} nodes` : p.stuck}`);
257
+ const t = 'tree' in p ? own({ tree: p.tree, from: part }, g2) : undefined;
258
+ if (t && 'tree' in p) {
259
+ n = n2;
260
+ main = t;
261
+ src = { tree: p.tree, from: part };
262
+ }
263
+ }
264
+ // Then the whole node, its goal sliced; it keeps the facts and uses its tree reads.
265
+ if (!main) {
266
+ const r = grown(g, n1.name, mine(n1.uses.length));
267
+ if ('proven' in r)
268
+ return r;
269
+ const used = sliceOn() ? timedKind('used', 0, r.used) : undefined;
270
+ const trim = used ? { ...n1, insts: n1.insts.filter((_, i) => used.has(h0 + i)), uses: n1.uses.filter((_, j) => used.has(base + j)) } : n1;
271
+ const g2 = trim.insts.length < n1.insts.length || trim.uses.length < n1.uses.length ? mainGoal(facts, shapes, trim) : g;
272
+ const t2 = own(r, g2);
273
+ main = t2 ?? own(r, g) ?? (() => { const o = grow(g); return 'tree' in o ? o.tree : undefined; })();
274
+ if (!main)
275
+ return { proven: false, checked: false, node: n1.name, goal: g.name, stuck: 'unknown' };
276
+ if (t2)
277
+ n = trim;
278
+ src = { tree: r.tree, from: r.from };
279
+ }
280
+ if (grownBefore.size >= 4096)
281
+ grownBefore.clear();
282
+ grownBefore.set(key, { tree: main, insts: n.insts.map(p => n1.insts.indexOf(p)), uses: n.uses.map(u => n1.uses.indexOf(u)) });
283
+ }
284
+ hidden = [];
285
+ // Each use's goals; where one isn't proven with the facts the node kept, again with all of them.
286
+ const usesOf = (n) => {
287
+ const nb = baseHyps(facts, n).length, mine2 = (i) => i < n.hyps.length || i >= nb, out = [];
288
+ for (const u of n.uses) {
289
+ const m = shapes[u.node];
290
+ if (!m)
291
+ return { proven: false, checked: false, node: n.name, goal: `use of node ${u.node}`, stuck: 'no such node' };
292
+ const premises = [];
293
+ for (const [i, c] of m.hyps.entries()) {
294
+ const t = whole(premiseGoal(facts, n, u, c, i), n.name, mine2);
295
+ if ('proven' in t)
296
+ return t;
297
+ premises.push(t);
298
+ }
299
+ const descent = whole(descentGoal(facts, n, u, m), n.name, mine2);
300
+ if ('proven' in descent)
301
+ return descent;
302
+ out.push({ ...u, premises, descent });
303
+ }
304
+ return out;
305
+ };
306
+ const uses = usesOf(n);
307
+ if (!('proven' in uses))
308
+ return { ...n, uses, main: main };
309
+ if (n.insts.length === n1.insts.length || !src)
310
+ return uses;
311
+ const n3 = { ...n, insts: n1.insts }, main3 = own(src, mainGoal(facts, shapes, n3)), uses3 = main3 ? usesOf(n3) : uses;
312
+ return main3 && !('proven' in uses3) ? { ...n3, uses: uses3, main: main3 } : uses;
313
+ };
314
+ for (const i of order) {
315
+ let failed;
316
+ for (const [j, spec] of [specs[i], ...(specs[i].variants ?? [])].entries()) {
317
+ // (a variant after the first within a few seconds a goal, one second once a node is refuted: a variant the first
318
+ // wasn't proven by is seldom needed, and seldom proves a node where another is refuted)
319
+ later = j ? (refuted ? 1_000 : 3_000) : undefined;
320
+ if (spec.uses.some(u => weak.has(u.node)))
321
+ continue;
322
+ const r = attempt(spec);
323
+ if ('proven' in r) {
324
+ failed ??= r;
325
+ if (r.stuck === 'sat')
326
+ refuted = true;
327
+ continue;
49
328
  }
50
- const descent = grown(descentGoal(facts, n, u, m), n.name);
51
- if ('proven' in descent)
52
- return descent;
53
- uses.push({ ...u, premises, descent });
329
+ nodes[i] = r;
330
+ shapes[i] = spec;
331
+ failed = undefined;
332
+ if (spec.weak)
333
+ weak.add(i);
334
+ break;
54
335
  }
55
- nodes.push({ ...n, uses, main });
336
+ if (failed || !nodes[i])
337
+ return failed ?? { proven: false, checked: false, node: specs[i].name, goal: specs[i].name, stuck: 'unknown' };
56
338
  }
57
- const proof = { facts, nodes }, why = refuseCyclic(proof);
339
+ const proof = { facts, nodes }, why = timedKind('check', 0, () => refuseCyclic(proof));
58
340
  return { proven: true, checked: why === undefined, proof, ...(why ? { why } : {}) };
59
341
  }
342
+ // An instance's nodes, and their variants, grown first on what their goals read.
343
+ const sliced = (specs) => specs.map(sp => ({ ...sp, slice: true, ...(sp.variants ? { variants: sp.variants.map(x => ({ ...x, slice: true })) } : {}) }));
60
344
  // The facts of a machine's functions (laws about every key of them), as cyclic facts.
61
345
  const factOf = (l) => ({ name: l.name, keys: [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])], holds: l.holds });
62
- export function recurNodes(r) {
346
+ // `usual`: the usual proofs of the lemmas (the recurrence run as a machine from x = 0) and of the laws between (y from x),
347
+ // translated (`translatedRecur` below), or no nodes with why not.
348
+ export function recurNodes(r, usual, st = tstats()) {
63
349
  const f = `${r.name}$`, x = v('x'), y = v('y'), kk = v('k$');
64
350
  const facts = [
65
351
  { name: `${r.name}(0)`, keys: [], holds: eq(get(f, k(0)), r.zero) },
@@ -81,11 +367,143 @@ export function recurNodes(r) {
81
367
  insts: [{ fact: 1, at: [less1(y)] }],
82
368
  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
369
  });
84
- return { facts, specs: withReads(facts, specs) };
370
+ if (!stepNodesOn())
371
+ return { facts, specs: sliced(withReads(facts, specs)) };
372
+ // 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)),
373
+ // 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
374
+ // 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
375
+ // the laws at y − 1 and the lemmas at x and y − 1.
376
+ const b = r.between.length, step = (e) => substE(r.next, vars({ x: less1(e), prev: get(f, less1(e)) }));
377
+ const atStep = (l) => substC(l.holds, vars({ x, fx: step(x) }));
378
+ const lemmaAfter = r.lemmas.map((l) => {
379
+ const name = `${r.name}: ${l.name}, after a step`, hyps = [le(k(1), x)];
380
+ 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 }] };
381
+ });
382
+ const betweenAfter = r.between.map((l) => {
383
+ const name = `${r.name}: ${l.name}, after a step`, hyps = [le(add(x, k(1)), y)];
384
+ return { name, hyps, concl: substC(l.holds, vars({ x, fx: get(f, x), y, fy: step(y) })), rank: [k(1), y, k(0)], insts: [],
385
+ uses: [...r.between.map((_, j) => ({ node: n + j, subst: { y: less1(y) }, guard: yes })), ...lemmaUses(x, yes), ...lemmaUses(less1(y), yes)],
386
+ variants: [{ name, hyps, concl: yes, rank: [k(1), y, k(0)], insts: [], uses: [], weak: true }] };
387
+ });
388
+ const own = specs.map((sp, i) => {
389
+ const lemma = i < n, a = { ...sp, rank: [...sp.rank, k(1)], insts: lemma ? sp.insts : [{ fact: 1, at: [less1(y)] }],
390
+ 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)],
391
+ variants: [{ ...sp, rank: [...sp.rank, k(1)] }] };
392
+ return a;
393
+ });
394
+ if (usual)
395
+ return { facts, ...translatedRecur() };
396
+ const all = withReads(facts, [...own, ...lemmaAfter, ...betweenAfter]);
397
+ for (const [i, a] of all.slice(0, n + b).entries()) {
398
+ const o = shared(facts, a, all[n + b + i].concl);
399
+ if (o.length)
400
+ all[i] = { ...a, opaque: o };
401
+ }
402
+ return { facts, specs: sliced(all.map(sp => (sp.variants ? { ...sp, variants: withReads(facts, sp.variants, all) } : sp))) };
403
+ // The usual proofs translated, as a machine's are: each lemma and law between three nodes. At x (at y), as above, its
404
+ // goal grown: from f(0) or the lemma at the start (x <= 0), or after a step to x (1 <= x). After a step, rank (0, x, 0):
405
+ // the usual step goal with x as x − 1 and fx as f(x − 1), the lemmas at x − 1 its uses; a law between likewise at y − 1
406
+ // (rank (1, y, 0)), fx as f(x), its uses the lemmas at x (the usual machine's facts) and at y − 1 and the laws between
407
+ // at y − 1 (each antecedent x <= y − 1 split first). At the start, rank (0, 0, 0) and (1, 0, 0): the usual start goal,
408
+ // fx0 as f(0) (a start that reads a function), fx as f(x).
409
+ function translatedRecur() {
410
+ const lemmaAfterT = [], betweenAfterT = [], lemmaStart = [], betweenStart = [];
411
+ const made = [], N = 2 * n + 2 * b;
412
+ const factIx = (name) => facts.findIndex((q, i) => i >= 2 && q.name === name);
413
+ // A usual goal's hypotheses: the machine's facts at the instances, its precondition and assumption, the laws assumed.
414
+ const layout = (u, i) => {
415
+ const g = obligations(u.m, u.laws, u.proof.instances, i), inst = u.proof.instances[u.laws[i].name] ?? {};
416
+ const fs = (u.m.facts ?? []).flatMap(q => { const ks = [...(q.forall ? [q.forall] : []), ...(q.keys ?? [])], flat = inst[q.name] ?? []; return ks.length ? Array.from({ length: Math.floor(flat.length / ks.length) }, (_, j) => ({ fact: q, at: flat.slice(j * ks.length, (j + 1) * ks.length) })) : [{ fact: q, at: [] }]; });
417
+ const np = u.m.pre ? 1 : 0, na = u.m.assume ? 1 : 0;
418
+ return { g0: g[0], g1: g[1], t0: u.proof.trees[g[0].name], t1: u.proof.trees[g[1].name], fs, np, na, ok: g[0].hyps.length === fs.length + np + 1 && g[1].hyps.length === fs.length + np + na + u.laws.length + 1 };
419
+ };
420
+ const last = (g) => g.hyps.at(-1).x;
421
+ if (n) {
422
+ const u = usual.lemmas;
423
+ if (!u || u.laws.map(l => l.name).join('\0') !== r.lemmas.map(l => l.name).join('\0') || u.laws.some(l => l.forall))
424
+ return { specs: [], why: 'lemmas: no usual proof of these lemmas' };
425
+ const sigma = vars({ x: less1(x), fx: get(f, less1(x)) }), sigma0 = vars({ fx0: get(f, k(0)) }), ren = renamer({ s: sigma }), ren0 = renamer({ s: sigma0 });
426
+ for (const [i] of r.lemmas.entries()) {
427
+ const lay = layout(u, i);
428
+ if (!lay.ok || !lay.t0 || !lay.t1)
429
+ return { specs: [], why: `${r.lemmas[i].name}: a usual goal of another shape` };
430
+ const used1 = usedHyps(lay.g1, lay.t1), used0 = usedHyps(lay.g0, lay.t0), insts1 = [], insts0 = [];
431
+ for (const [q, p] of lay.fs.entries()) {
432
+ const zeroFact = p.fact.name.startsWith('fx0 is '), ix = zeroFact ? 0 : factIx(p.fact.name);
433
+ for (const [used, out, s] of [[used1, insts1, sigma], [used0, insts0, sigma0]]) {
434
+ if (!used.has(q))
435
+ continue;
436
+ if (ix < 0 || (zeroFact && s === sigma))
437
+ return { specs: [], why: `${p.fact.name}: no fact of the recurrence's` };
438
+ out.push({ fact: ix, at: p.at.map(e => substE(e, s)) });
439
+ }
440
+ }
441
+ const pre = u.m.pre ? [substC(u.m.pre, sigma)] : [];
442
+ lemmaAfterT.push({ name: `${r.name}: ${r.lemmas[i].name}, after a step`, hyps: [le(k(1), x), ...pre, ...(u.m.assume ? [substC(u.m.assume, sigma)] : [])], concl: substC(last(lay.g1), sigma), rank: [k(0), x, k(0)],
443
+ insts: insts1, uses: lemmaUses(less1(x), yes).filter((_, j) => used1.has(lay.fs.length + lay.np + lay.na + j)) });
444
+ lemmaStart.push({ name: `${r.name}: ${r.lemmas[i].name}, at the start`, hyps: u.m.pre ? [u.m.pre] : [], concl: substC(last(lay.g0), sigma0), rank: [k(0), k(0), k(0)], insts: insts0, uses: [] });
445
+ made.push({ g: lay.g1, tree: lay.t1, node: n + b + i, ren }, { g: lay.g0, tree: lay.t0, node: N + i, ren: ren0 });
446
+ }
447
+ }
448
+ if (b) {
449
+ const u = usual.between, lemmaAt = (name) => r.lemmas.findIndex(l => `${l.name} at x` === name);
450
+ if (!u || u.laws.slice(n).map(l => l.name).join('\0') !== r.between.map(l => l.name).join('\0') || u.laws.some(l => l.forall))
451
+ return { specs: [], why: 'between: no usual proof of these laws' };
452
+ const sigma = vars({ y: less1(y), fy: get(f, less1(y)), fx: get(f, x) }), sigma0 = vars({ fx: get(f, x) }), ren = renamer({ s: sigma }), ren0 = renamer({ s: sigma0 });
453
+ for (const [i] of r.between.entries()) {
454
+ const lay = layout(u, n + i);
455
+ if (!lay.ok || !lay.t0 || !lay.t1)
456
+ return { specs: [], why: `${r.between[i].name}: a usual goal of another shape` };
457
+ const used1 = usedHyps(lay.g1, lay.t1), used0 = usedHyps(lay.g0, lay.t0);
458
+ const parts = (used, s, after) => {
459
+ const insts = [], uses = [];
460
+ for (const [q, p] of lay.fs.entries()) {
461
+ if (!used.has(q))
462
+ continue;
463
+ const j = lemmaAt(p.fact.name), ix = factIx(p.fact.name);
464
+ if (j >= 0)
465
+ uses.push({ node: j, subst: { x }, guard: yes });
466
+ else if (ix >= 0)
467
+ insts.push({ fact: ix, at: p.at.map(e => substE(e, s)) });
468
+ else
469
+ return undefined;
470
+ }
471
+ if (after)
472
+ for (const [j] of u.laws.entries())
473
+ if (used.has(lay.fs.length + lay.np + lay.na + j))
474
+ uses.push(j < n ? { node: j, subst: { x: less1(y) }, guard: yes } : { node: j, subst: { y: less1(y) }, guard: yes });
475
+ return { insts, uses };
476
+ };
477
+ const p1 = parts(used1, sigma, true), p0 = parts(used0, sigma0, false);
478
+ if (!p1 || !p0)
479
+ return { specs: [], why: `${r.between[i].name}: a fact of no recurrence's` };
480
+ const pre = u.m.pre ? [substC(u.m.pre, sigma)] : [];
481
+ betweenAfterT.push({ name: `${r.name}: ${r.between[i].name}, after a step`, hyps: [le(add(x, k(1)), y), ...pre, ...(u.m.assume ? [substC(u.m.assume, sigma)] : [])], concl: substC(last(lay.g1), sigma), rank: [k(1), y, k(0)], ...p1 });
482
+ betweenStart.push({ name: `${r.name}: ${r.between[i].name}, at the start`, hyps: u.m.pre ? [u.m.pre] : [], concl: substC(last(lay.g0), sigma0), rank: [k(1), k(0), k(0)], ...p0 });
483
+ made.push({ g: lay.g1, tree: lay.t1, node: 2 * n + b + i, ren, decide: p1.uses.flatMap(us => (us.node >= n && us.node < n + b ? [le(x, less1(y))] : [])) }, { g: lay.g0, tree: lay.t0, node: N + n + i, ren: ren0 });
484
+ }
485
+ }
486
+ const carried = own.map((sp, i) => ({ ...sp, variants: undefined, carry: true, uses: [...sp.uses, i < n ? { node: N + i, subst: {}, guard: le(x, k(0)) } : { node: N + i, subst: {}, guard: le(y, x) }] }));
487
+ const rest = [...lemmaAfterT, ...betweenAfterT, ...lemmaStart, ...betweenStart], all = [...withReads(facts, carried, [...carried, ...rest]), ...rest];
488
+ for (const [i, a] of all.slice(0, n + b).entries()) {
489
+ const o = [...new Set([...shared(facts, a, all[n + b + i].concl), ...shared(facts, a, all[N + i].concl)].map(e => JSON.stringify(e)))].map(e => JSON.parse(e));
490
+ if (o.length)
491
+ all[i] = { ...a, opaque: o };
492
+ }
493
+ const until = performance.now() + 10_000;
494
+ for (const m of made) {
495
+ const whole = mainGoal(facts, all, all[m.node]);
496
+ const tree = timedKind('translate', 0, () => withProducts(() => decide(whole, m.decide ?? [], wp => translate(m.g, whole, m.tree, m.ren, st, until, new Map(), wp), st, until)));
497
+ if (!tree)
498
+ return { specs: [], why: `${all[m.node].name}: not translated` };
499
+ all[m.node] = { ...all[m.node], main: tree };
500
+ }
501
+ return { specs: sliced(all) };
502
+ }
85
503
  }
86
504
  // 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
505
  // alone) at each key f is read at, twice over (a fact's own reads give more keys).
88
- function withReads(facts, specs) {
506
+ function withReads(facts, specs, nodes = specs) {
89
507
  const simple = facts.flatMap((f, i) => {
90
508
  if (f.keys.length !== 1)
91
509
  return [];
@@ -104,7 +522,7 @@ function withReads(facts, specs) {
104
522
  return specs.map(n => {
105
523
  const insts = [...n.insts], seen = new Set(insts.map(p => JSON.stringify(p)));
106
524
  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(specs[u.node].concl, vars(u.subst)))];
525
+ 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
526
  for (const s of simple)
109
527
  for (const key of readsIn(goal, s.map)) {
110
528
  const p = { fact: s.fact, at: [key] }, id = JSON.stringify(p);
@@ -122,7 +540,9 @@ function withReads(facts, specs) {
122
540
  // 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
123
541
  // the prover would have to prove equal through products (`machineNodes` proves those laws far slower than the usual
124
542
  // rule, past its budget: research/cyclic/probe.ts).
125
- export const cyclicMachine = (m, laws = []) => {
543
+ export const cyclicMachine = (m, laws = []) => uncoveredWhy(m, laws) === undefined;
544
+ // Why a machine's laws are no instance's (undefined where they are one).
545
+ export const uncoveredWhy = (m, laws = []) => {
126
546
  const maps = Object.keys(m.maps ?? {});
127
547
  const nested = (t) => { let found = false; const walk = (x, inKey) => { if (found || !x || typeof x !== 'object')
128
548
  return; const o = x; if (o.op === 'get' && inKey && maps.includes(o.map)) {
@@ -133,7 +553,7 @@ export const cyclicMachine = (m, laws = []) => {
133
553
  return;
134
554
  } for (const val of Object.values(o))
135
555
  walk(val, inKey); }; walk(t, false); return found; };
136
- return !m.history && !Object.values(m.step.flags ?? {}).some(c => c.op === 'all') && !laws.some(l => nested(l.holds));
556
+ return m.history ? 'a history' : Object.values(m.step.flags ?? {}).some(c => c.op === 'all') ? 'a flag at every key' : laws.some(l => nested(l.holds)) ? 'a law reading a map at a key read from a map' : undefined;
137
557
  };
138
558
  // The run as maps over time: names at time u read as their maps there.
139
559
  function trace(m) {
@@ -166,7 +586,8 @@ const flagsOf = (m) => {
166
586
  out[f] = substC(c, { vars: {}, maps: {}, flags: out });
167
587
  return out;
168
588
  };
169
- export function machineNodes(m, laws, instances) {
589
+ // `lawsMade`: the laws' nodes are made already (their trees not translated again).
590
+ export function machineNodes(m, laws, instances, usual, st = tstats()) {
170
591
  const { at } = trace(m), flags = flagsOf(m), inline = (t) => substC(t, { vars: {}, maps: {}, flags });
171
592
  const t = v('t$'), T = v('T$'), u = v('u$'), key = v('k$');
172
593
  const inlineE = (e) => substE(e, { vars: {}, maps: {}, flags });
@@ -197,6 +618,14 @@ export function machineNodes(m, laws, instances) {
197
618
  { name: `${m.name}: params are naturals`, keys: [], holds: and(yes, ...m.params.map(x => le(k(0), v(x)))) },
198
619
  ];
199
620
  const fi = (name) => facts.findIndex(f => f.name === name);
621
+ // The run's facts at time t: the start and the step (Bridge.lean's cycCanon has them so).
622
+ const run = (when) => [{ fact: 0, at: [when] }, { fact: 1, at: [when] }];
623
+ 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)]] : []; }));
624
+ const afterStep = (e) => {
625
+ const walk = (x) => (Array.isArray(x) ? x.map(walk) : !x || typeof x !== 'object' ? x
626
+ : nexts.get(JSON.stringify(x)) ?? Object.fromEntries(Object.entries(x).map(([key, val]) => [key, walk(val)])));
627
+ return nexts.size ? walk(e) : e;
628
+ };
200
629
  const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
201
630
  // A map read at the node's time t at key e: that map's start, step and natural facts at (t, e).
202
631
  const mapInsts = (c) => {
@@ -221,12 +650,86 @@ export function machineNodes(m, laws, instances) {
221
650
  walk(c);
222
651
  return out;
223
652
  };
653
+ // Each law twice (GDP_CYCLIC_STEP=0 states the first alone, its goal the whole step): at time t, rank (t, 1), and after
654
+ // 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
655
+ // 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
656
+ // 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
657
+ // assumes them at, with the functions' facts at the keys it names, as the usual rule's goal does.
658
+ function stepNodes(single) {
659
+ const L = laws.length, pre = at(less1(t)), one = le(k(1), t);
660
+ const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
661
+ const over = (e, frame) => substE(substE(e, vars(fresh)), frame);
662
+ // The state the step gives, over the frame before it.
663
+ 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) => {
664
+ const w = m.step.maps?.[map];
665
+ // (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)
666
+ 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) };
667
+ }])) };
668
+ const omegaHyp = mapNames.length ? [{ op: '<', a: T, b: omega }] : [];
669
+ const fnInsts = (inst, frame) => (m.facts ?? []).flatMap(f => {
670
+ const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
671
+ if (!ks.length)
672
+ return [{ fact, at: [] }];
673
+ 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)) }));
674
+ });
675
+ const naturals = (when) => state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [when] }));
676
+ const params = { fact: fi(`${m.name}: params are naturals`), at: [] };
677
+ // The law at t first with the start's and the step's facts alone, then with the functions' facts at the keys the
678
+ // machine's proof names (read at t: the start's case), then as one node whose goal is the whole step (its uses the
679
+ // laws at t − 1), for a law after the step that isn't proven (that node then holds outright, a weak variant).
680
+ const nowNodes = laws.map((l, i) => {
681
+ const concl = substC(l.holds, at(t)), inst = instances[l.name] ?? {};
682
+ const hyps = [...(m.pre ? [m.pre] : []), le(t, T), ...omegaHyp], uses = [{ node: L + i, subst: {}, guard: one }];
683
+ const lean = [...run(t), ...mapInsts(concl), ...naturals(t), params];
684
+ const fns = fnInsts(inst, at(t));
685
+ return { name: `${m.name}: ${l.name}`, hyps, concl, rank: [t, k(1)], insts: lean, uses,
686
+ variants: [...(fns.length ? [{ name: `${m.name}: ${l.name}`, hyps, concl, rank: [t, k(1)], insts: [...lean, ...fns], uses }] : []), { ...single[i], rank: [t, k(1)] }] };
687
+ });
688
+ const stepNodes = laws.map((l) => {
689
+ const inst = instances[l.name] ?? {}, concl = substC(substC(substC(l.holds, vars(fresh)), upd), pre);
690
+ const uses = laws.flatMap((l2, j) => {
691
+ const ks = keysOf(l2);
692
+ if (!ks.length)
693
+ return [{ node: j, subst: { 't$': less1(t) }, guard: one }];
694
+ const flat = inst[l2.name] ?? [];
695
+ 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 }));
696
+ });
697
+ // (the request the step assumes a hypothesis of its own, which the law at t has from the step's fact: the step's
698
+ // equations are no hypothesis of this goal)
699
+ 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`;
700
+ return { name, hyps, concl, rank: [t, k(0)], insts: [...fnInsts(inst, pre), ...naturals(less1(t)), params], uses,
701
+ variants: [{ name, hyps, concl: yes, rank: [t, k(0)], insts: [], uses: [], weak: true }] };
702
+ });
703
+ // 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
704
+ // gives) are opaque to its search, so that it carries the one to the other without splitting the step's cases.
705
+ for (const [i, a] of nowNodes.entries()) {
706
+ const o = shared(facts, a, stepNodes[i].concl);
707
+ if (o.length)
708
+ a.opaque = o;
709
+ }
710
+ return [...nowNodes, ...stepNodes];
711
+ }
224
712
  const specs = laws.map(l => {
225
713
  const inst = instances[l.name] ?? {};
226
714
  // Terms over the frame before the step (the machine's proof proves a law about every key at a fresh key k#: here
227
715
  // the node's own k).
228
716
  const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
229
717
  const pre = at(less1(t)), before = (e) => substE(substE(e, vars(fresh)), pre);
718
+ // A function's fact also at its keys with the terms the step gives a state variable (the usual proof reads the state
719
+ // 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
720
+ // key its own reads are at, not one the prover would have to prove equal to it.
721
+ const fnReads = (x) => { const out = []; const walk = (y) => { if (y && typeof y === 'object') {
722
+ const o = y;
723
+ if (o.op === 'get' && !o.map.endsWith('@'))
724
+ out.push(JSON.stringify(o));
725
+ Object.values(o).forEach(walk);
726
+ } }; walk(x); return out; };
727
+ const concl = substC(l.holds, at(t)), reads = new Set(fnReads(concl));
728
+ const after = (e) => substE(afterStep(substE(e, vars(fresh))), pre);
729
+ const both = (tuple, make, read) => {
730
+ const a = make(tuple.map(before)), b = make(tuple.map(after));
731
+ return JSON.stringify(a) === JSON.stringify(b) || !fnReads(read(b)).some(c => reads.has(c)) ? [a] : [a, b];
732
+ };
230
733
  // The laws at the step before, at the keys the machine's proof assumes them at.
231
734
  const uses = laws.flatMap((l2, j) => {
232
735
  const ks = keysOf(l2);
@@ -237,25 +740,431 @@ export function machineNodes(m, laws, instances) {
237
740
  });
238
741
  // The functions' facts at the keys the proof names, over the frame before the step (at t = 0, the start's).
239
742
  const factInsts = (m.facts ?? []).flatMap(f => {
240
- const ks = keysOf(f), flat = inst[f.name] ?? [];
743
+ const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
241
744
  if (!ks.length)
242
- return [{ fact: fi(f.name), at: [] }];
243
- return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ fact: fi(f.name), at: flat.slice(i * ks.length, (i + 1) * ks.length).map(before) }));
745
+ return [{ fact, at: [] }];
746
+ 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
747
  });
245
- const concl = substC(l.holds, at(t));
246
748
  const useConcls = uses.map(us => substC(substC(laws[us.node].holds, at(t)), vars(us.subst)));
247
749
  const base = mapInsts([concl, useConcls]);
248
750
  return {
249
751
  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: [{ fact: 0, at: [t] }, { fact: 1, at: [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,
752
+ 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
753
  };
252
754
  });
253
- return { facts, specs };
755
+ if (usual)
756
+ return { facts, ...translatedNodes() };
757
+ return { facts, specs: sliced(stepNodesOn() ? stepNodes(specs) : specs) };
758
+ // The usual proof translated (core/translate.ts): each law three nodes. At t, rank (t, 1), as above (Bridge.lean's
759
+ // cycMOk reads it), its goal grown: it carries the law at the start or after t's step to the cells at t through the
760
+ // start's and the step's equations. After a step from t − 1, rank (t, 0): the usual step goal over the run's cells at
761
+ // t − 1 (each hypothesis of it read so: the functions' facts at the keys the usual proof names, the precondition, the
762
+ // request's assumption, every law at t − 1 at those keys, each a use guarded by 0 <= 0, its descent from 1 <= t), its
763
+ // tree the usual one's renamed. At the start, rank (0, 0): the usual start goal (the law at the start's values), the
764
+ // law's fresh key k# its own k, its tree the usual one's. Only the facts and uses the usual tree reads are kept.
765
+ function translatedNodes() {
766
+ const L = laws.length, pre = at(less1(t)), one = le(k(1), t), omegaHyp = mapNames.length ? [{ op: '<', a: T, b: omega }] : [];
767
+ const fresh = Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : [])));
768
+ const sigma = { vars: { ...pre.vars, ...fresh }, maps: pre.maps, flags: {} };
769
+ const ren = renamer({ s: sigma, cells: Object.fromEntries([...mapNames, ...(m.inputMaps ?? [])].map(map => [map, { to: `${map}@`, at: less1(t) }])) }), ren0 = renamer({ s: vars(fresh) });
770
+ const fnInsts = (inst, frame) => (m.facts ?? []).flatMap(f => {
771
+ const ks = keysOf(f), flat = inst[f.name] ?? [], fact = fi(f.name);
772
+ if (!ks.length)
773
+ return [{ fact, at: [] }];
774
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => ({ fact, at: flat.slice(i * ks.length, (i + 1) * ks.length).map(e => substE(substE(e, vars(fresh)), frame)) }));
775
+ });
776
+ const goals = new Map(obligations(m, laws, instances).map(g => [g.name, g]));
777
+ // The state the step gives over the frame before it, a write's reads at its key read at the key read instead (as the
778
+ // map's step fact has them): the usual step goal's conclusion but for those reads, which the translation reads at
779
+ // the key read where the usual path has the two keys equal (two cells over time then split apart: `close`).
780
+ const upd = { flags: {}, vars: Object.fromEntries(state.map(st2 => [st2, inlineE(m.step.state?.[st2] ?? v(st2))])), maps: Object.fromEntries(mapNames.map(map => [map, (kk) => {
781
+ const w = m.step.maps?.[map];
782
+ 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) };
783
+ }])) };
784
+ // A step law's condition with its primed names read as given, its flags inlined (over the frame before the step).
785
+ const primedOf = (c, s0) => substC(c, { ...s0, flags });
786
+ const nows = [], afters = [], starts = [], made = [];
787
+ // A write's key and the keys the usual conclusion reads its map at (each a condition if(K = W, ...)).
788
+ const wkeys = new Set(mapNames.flatMap(map => { const w = m.step.maps?.[map]; return w && !('each' in w) ? [JSON.stringify(inlineE(w.key))] : []; }));
789
+ const readsOf = (c) => {
790
+ const out = [], seen = new Set();
791
+ const walk = (x) => {
792
+ if (!x || typeof x !== 'object')
793
+ return;
794
+ const o = x;
795
+ if (o.op === 'if' && o.c.op === '=' && wkeys.has(JSON.stringify(o.c.b))) {
796
+ const id = JSON.stringify([o.c.a, o.c.b]);
797
+ if (!seen.has(id)) {
798
+ seen.add(id);
799
+ out.push({ k: o.c.a, w: o.c.b });
800
+ }
801
+ }
802
+ for (const val of Object.values(o))
803
+ walk(val);
804
+ };
805
+ walk(c);
806
+ return out;
807
+ };
808
+ for (const [i, l] of laws.entries()) {
809
+ const g0 = goals.get(`${l.name} @start`), g1 = goals.get(`${l.name} @step`), t0 = g0 && usual.trees[g0.name], t1 = g1 && usual.trees[g1.name];
810
+ if (!g0 || !g1 || !t0 || !t1)
811
+ return { specs: [], why: `${l.name}: no usual tree` };
812
+ const inst = instances[l.name] ?? {}, nf = assumed(m.facts ?? [], inst).length, np = m.pre ? 1 : 0, na = m.assume ? 1 : 0;
813
+ const uses = laws.flatMap((l2, j) => {
814
+ const ks = keysOf(l2);
815
+ if (!ks.length)
816
+ return [{ node: j, subst: { 't$': less1(t) }, guard: yes }];
817
+ const flat = inst[l2.name] ?? [];
818
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, q) => ({ node: j, subst: { 't$': less1(t), ...Object.fromEntries(ks.map((kn, r) => [kn, substE(flat[q * ks.length + r], sigma)])) }, guard: yes }));
819
+ });
820
+ const fn1 = fnInsts(inst, pre), fn0 = fnInsts(inst, vars({}));
821
+ if (g1.hyps.length !== nf + np + na + uses.length + 1 || g0.hyps.length !== nf + np + 1 || fn1.length !== nf)
822
+ return { specs: [], why: `${l.name}: a usual goal of another shape` };
823
+ const used1 = usedHyps(g1, t1), used0 = usedHyps(g0, t0), last = (g) => g.hyps.at(-1).x;
824
+ const concl = substC(l.holds, at(t));
825
+ afters.push({ name: `${m.name}: ${l.name}, after a step`, hyps: [...(m.pre ? [m.pre] : []), one, le(t, T), ...omegaHyp, ...(m.assume ? [now(inline(m.assume), less1(t))] : [])],
826
+ concl: substC(substC(substC(l.holds, vars(fresh)), upd), pre), rank: [t, k(0)], insts: [...fn1.filter((_, q) => used1.has(q))], uses: uses.filter((_, j) => used1.has(nf + np + na + j)) });
827
+ starts.push({ name: `${m.name}: ${l.name}, at the start`, hyps: m.pre ? [m.pre] : [], concl: substC(last(g0), vars(fresh)), rank: [k(0), k(0)], insts: [...fn0.filter((_, q) => used0.has(q))], uses: [] });
828
+ nows.push({ name: `${m.name}: ${l.name}`, hyps: [...(m.pre ? [m.pre] : []), le(t, T), ...omegaHyp], concl, rank: [t, k(1)], insts: [...run(t), ...mapInsts(concl)],
829
+ uses: [{ node: L + i, subst: {}, guard: one }, { node: 2 * L + i, subst: {}, guard: le(t, k(0)) }], carry: true });
830
+ made.push({ g: g1, tree: t1, node: L + i, ren, decide: [one], reads: readsOf(last(g1)) }, { g: g0, tree: t0, node: 2 * L + i, ren: ren0 });
831
+ }
832
+ // One-step step laws (`usual.steps`), each two nodes after the laws' (core/cyclic.ts's header has their shape): after a
833
+ // step from t − 1, rank (t, 0), the usual step law's goal over the run's cells at t − 1 (its `when` and the request's
834
+ // assumption hypotheses, the laws at t − 1 at its keys uses), the state after it the step's terms; and the step law
835
+ // itself, rank (t, 2), over the cells at t − 1 and at t (a primed name at t), carried from the first by the step's
836
+ // facts at t, its hypotheses the precondition, 1 <= t <= T, T < Ω and `when` at t − 1.
837
+ const stepSpecs = [];
838
+ for (const [q, u] of (usual.steps ?? []).entries()) {
839
+ const { sl, inst, goal: g, tree } = u, whens = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when], K = sl.steps ?? 1;
840
+ // (k steps: a machine with no map, the state after each step a cell at its time; one step: any the instances cover)
841
+ if (K > 1 && (mapNames.length || m.inputMaps?.length))
842
+ return { specs: [], why: `${sl.name}: steps over a map` };
843
+ // The times the steps start from (t − k, ..., t − 1), and the frames: the state before step j its cells at t − k + j − 1
844
+ // (n~j the state after step j, at t − k + j), its request read there (i@j).
845
+ const time = (back) => (back === 0 ? t : { op: 'sub', a: t, b: k(back) });
846
+ const frame = { vars: { ...at(time(K)).vars, ...Object.fromEntries(Array.from({ length: K }, (_, j) => j + 1).flatMap(j => [
847
+ ...(j >= 2 ? m.inputs.map(i => [`${i}@${j}`, get(`${i}@`, time(K - j + 1))]) : []),
848
+ ...(j < K ? state.map(n => [`${n}~${j}`, get(`${n}@`, time(K - j))]) : []),
849
+ ])) }, maps: pre.maps, flags: {} };
850
+ const sig = K === 1 ? sigma : frame, renK = K === 1 ? ren : renamer({ s: frame });
851
+ // (the usual goal's hypotheses: the precondition, the facts at the instances, then each step's laws, assumption and
852
+ // `when`, and after each step but the last its state's equations)
853
+ const nf = assumed(m.facts ?? [], inst).length, np = m.pre ? 1 : 0, na = m.assume ? 1 : 0, nl = assumed(laws, inst).length;
854
+ const lawsAt = (back) => laws.flatMap((l2, j) => {
855
+ const ks = keysOf(l2), tb = time(back), ab = at(tb);
856
+ if (!ks.length)
857
+ return [{ node: j, subst: { 't$': tb }, guard: yes }];
858
+ const flat = inst[l2.name] ?? [];
859
+ return Array.from({ length: Math.floor(flat.length / ks.length) }, (_, r) => ({ node: j, subst: { 't$': tb, ...Object.fromEntries(ks.map((kn, c) => [kn, substE(flat[r * ks.length + c], ab)])) }, guard: yes }));
860
+ });
861
+ const perStep = Array.from({ length: K }, (_, j) => lawsAt(K - j)), fn1 = fnInsts(inst, sig);
862
+ // Each step's block: its laws, its assumption, its when, then (but after the last) its state's equations.
863
+ const blocks = [];
864
+ let pos = np + nf;
865
+ for (let j = 0; j < K; j++) {
866
+ blocks.push(pos);
867
+ pos += nl + na + (whens[j] ? 1 : 0) + (j < K - 1 ? state.length : 0);
868
+ }
869
+ if (g.hyps.length !== pos + 1 || fn1.length !== nf || perStep.some(us => us.length !== nl))
870
+ return { specs: [], why: `${sl.name}: a usual goal of another shape` };
871
+ const used = usedHyps(g, tree), last = g.hyps.at(-1).x;
872
+ const assumes = Array.from({ length: K }, (_, j) => (m.assume ? [now(inline(m.assume), time(K - j))] : [])).flat();
873
+ const when = whens.map((w, j) => now(inline(w), time(K - j)));
874
+ const bounds = [...(m.pre ? [m.pre] : []), le(k(K), t), le(t, T), ...omegaHyp];
875
+ const after = K === 1 ? substC(primedOf(sl.then, { vars: Object.fromEntries(state.map(st2 => [`${st2}'`, inlineE(m.step.state?.[st2] ?? v(st2))])), maps: Object.fromEntries(mapNames.map(map => [`${map}'`, upd.maps[map]])), flags: {} }), pre) : substC(last, frame);
876
+ const later = Object.fromEntries(Array.from({ length: K - 1 }, (_, j) => j + 2).flatMap(j => m.inputs.map(i => [`${i}@${j}`, get(`${i}@`, time(K - j + 1))])));
877
+ const cells = substC(primedOf(sl.then, { vars: { ...later, ...Object.fromEntries(state.map(st2 => [`${st2}'`, get(`${st2}@`, t)])) }, maps: Object.fromEntries(mapNames.map(map => [`${map}'`, (kk) => get(`${map}@`, pair(t, kk))])), flags: {} }), at(time(K)));
878
+ const a = 3 * L + 2 * q, steps = Array.from({ length: K - 1 }, (_, j) => ({ fact: 1, at: [time(K - j - 1)] }));
879
+ // (the state after each step but the last its equations, hypotheses here as in the usual goal: the step's facts at
880
+ // those times give them where the step law is, `steps` below)
881
+ const eqs = blocks.slice(0, K - 1).flatMap((b0, j) => g.hyps.slice(b0 + nl + na + (whens[j] ? 1 : 0), b0 + nl + na + (whens[j] ? 1 : 0) + state.length).map(h => substC(h, frame)));
882
+ stepSpecs.push({ name: `${m.name}: ${sl.name}, after ${K === 1 ? 'a step' : `${K} steps`}`, hyps: [...bounds, ...assumes, ...when, ...eqs], concl: after, rank: [t, k(0)],
883
+ insts: fn1.filter((_, r) => used.has(np + r)), uses: perStep.flatMap((us, j) => us.filter((_, r) => used.has(blocks[j] + r))) });
884
+ stepSpecs.push({ name: `${m.name}: ${sl.name}`, hyps: [...bounds, ...when], concl: cells, rank: [t, k(2)],
885
+ insts: [...run(t), ...steps, ...mapInsts(cells)], uses: [{ node: a, subst: {}, guard: yes }], carry: true });
886
+ // (each time decided at least the steps before it first: t ∸ j is then t − j)
887
+ made.push({ g, tree, node: a, ren: renK, decide: Array.from({ length: K }, (_, j) => le(k(j + 1), t)), reads: K === 1 ? readsOf(last) : [] });
888
+ }
889
+ const all = [...nows, ...afters, ...starts, ...stepSpecs];
890
+ for (const [i, a] of nows.entries()) {
891
+ const o = [...new Set([...shared(facts, a, afters[i].concl), ...shared(facts, a, starts[i].concl)].map(e => JSON.stringify(e)))].map(e => JSON.parse(e));
892
+ if (o.length)
893
+ a.opaque = o;
894
+ }
895
+ // (a translation is bounded by a minute: a step law over 32 steps has a goal of 32 frames at every leaf)
896
+ const until = performance.now() + 60_000;
897
+ for (const x of made) {
898
+ if (usual.lawsMade && x.node < 3 * L)
899
+ continue;
900
+ // (after a step, 1 <= t decided first: t ∸ 1 is then t − 1, and a cell read at a key over the cells at t − 1 has a
901
+ // linear form)
902
+ const whole = mainGoal(facts, all, all[x.node]);
903
+ const tree = timedKind('translate', 0, () => withProducts(() => decide(whole, x.decide ?? [], wp => readsFirst(whole, x.reads ?? [], x.ren, (p, swaps) => translate(x.g, whole, x.tree, x.ren, st, until, new Map(), p, swaps), st, until, wp), st, until)));
904
+ if (!tree)
905
+ return { specs: [], why: `${all[x.node].name}: not translated` };
906
+ all[x.node] = { ...all[x.node], main: tree };
907
+ }
908
+ return { specs: sliced(all) };
909
+ }
910
+ }
911
+ // The terms (not a read, a variable or a constant) of a conclusion that a node's facts read too, largest first; none
912
+ // inside a product (a product of a term's linear form is no linear form).
913
+ function shared(facts, a, concl) {
914
+ const inst = instHyps(facts, a.insts), theirs = JSON.stringify(inst), seen = new Set(), out = [], factors = new Set();
915
+ const inProducts = (x, under = false) => {
916
+ if (!x || typeof x !== 'object')
917
+ return;
918
+ if (Array.isArray(x)) {
919
+ x.forEach(y => inProducts(y, under));
920
+ return;
921
+ }
922
+ const o = x;
923
+ if (under)
924
+ factors.add(JSON.stringify(o));
925
+ for (const val of Object.values(o))
926
+ inProducts(val, under || o.op === 'prod');
927
+ };
928
+ inProducts([concl, inst]);
929
+ const walk = (x) => {
930
+ if (!x || typeof x !== 'object')
931
+ return;
932
+ if (Array.isArray(x)) {
933
+ x.forEach(walk);
934
+ return;
935
+ }
936
+ const o = x, id = JSON.stringify(o);
937
+ if (['if', 'add', 'sub', 'min', 'max', 'mul', 'prod'].includes(o.op) && theirs.includes(id) && !factors.has(id)) {
938
+ if (!seen.has(id)) {
939
+ seen.add(id);
940
+ out.push(o);
941
+ }
942
+ return;
943
+ }
944
+ for (const val of Object.values(o))
945
+ walk(val);
946
+ };
947
+ walk(concl);
948
+ return out.sort((x, y) => JSON.stringify(y).length - JSON.stringify(x).length);
949
+ }
950
+ // --- step laws and k-step laws over the run -----------------------------------------------------------------------
951
+ // A k-step law (or a step law, k = 1) as a node over the machine's run (BridgeRules.lean's cycKOk, cyc_kstep_sound
952
+ // carries it to the law): the run's last k steps up to time t, each time one step back from the next (t − 1, t − 1 − 1,
953
+ // …), as a step's fact reads the time before it, so the cells a step gives are those the next one reads. The first state
954
+ // at t − k, the j-th request at t − (k − j), the state after the last at t.
955
+ export const tBack = (n) => (n === 0 ? v('t$') : less1(tBack(n - 1)));
956
+ // The same times read t − n at once (BridgeRules.lean's tSub; core/translate.ts's step laws over several steps).
957
+ export const tSub = (n) => (n === 0 ? v('t$') : { op: 'sub', a: v('t$'), b: k(n) });
958
+ // `then` over the run (BridgeRules.lean's kσ and kRd): the primed names at t, each later request's inputs (input@j) and
959
+ // the maps it carries (map@j) at its time, the plain names at t − k.
960
+ export function kStepAt(m, k, time = tBack) {
961
+ const { at } = trace(m), first = at(time(k)), last = at(time(0));
962
+ const vars = { ...first.vars }, maps = { ...first.maps };
963
+ for (const x of Object.keys(m.state))
964
+ vars[`${x}'`] = last.vars[x];
965
+ for (const map of Object.keys(m.maps ?? {}))
966
+ maps[`${map}'`] = last.maps[map];
967
+ for (let j = 1; j < k; j++) {
968
+ const a = at(time(k - j));
969
+ for (const i of m.inputs)
970
+ vars[`${i}@${j + 1}`] = a.vars[i];
971
+ for (const c of m.inputMaps ?? [])
972
+ maps[`${c}@${j + 1}`] = a.maps[c];
973
+ }
974
+ return { vars, maps, flags: {} };
975
+ }
976
+ // A k-step law's `when`s, one per step (none: 0 <= 0), its flags inlined, as Lean's SKStep has them.
977
+ export const kWhens = (m, sl) => {
978
+ const ws = sl.when === undefined ? [] : Array.isArray(sl.when) ? sl.when : [sl.when], flags = flagsOf(m);
979
+ return Array.from({ length: sl.steps ?? 1 }, (_, j) => (ws[j] ? substC(ws[j], { vars: {}, maps: {}, flags }) : yes));
980
+ };
981
+ // The nodes of the step laws, after the laws' (`laws` the machine's, `lawNodes` their nodes, the first L at time t, as
982
+ // machineNodes makes them). Each node: the precondition, k <= t <= T, T < Ω and each step's `when` at its time;
983
+ // the run's facts at the times it reads (each step's, each map cell's step back from where it is read, the functions'
984
+ // facts at the keys the usual proof names), every law at each state but the last (at the keys it names), and `then`.
985
+ export function stepLawNodes(m, laws, steps, facts, lawNodes, time = tBack) {
986
+ const rank = lawNodes[0]?.rank.length ?? 2;
987
+ const { at } = trace(m), flags = flagsOf(m), inline = (c) => substC(c, { vars: {}, maps: {}, flags });
988
+ const t = v('t$'), T = v('T$'), mapNames = Object.keys(m.maps ?? {}), traced = [...mapNames, ...(m.inputMaps ?? [])], state = Object.keys(m.state);
989
+ const fi = (name) => facts.findIndex(f => f.name === name);
990
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
991
+ const tuples = (inst, l) => { const ks = keysOf(l), flat = inst[l.name] ?? []; return ks.length ? Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => flat.slice(i * ks.length, (i + 1) * ks.length)) : [[]]; };
992
+ const specs = steps.map(({ law: sl, instances: inst }) => {
993
+ const K = sl.steps ?? 1, times = Array.from({ length: K + 1 }, (_, j) => time(K - j));
994
+ const hyps = [...(m.pre ? [m.pre] : []), le(k(K), t), le(t, T), ...(traced.length ? [{ op: '<', a: T, b: omega }] : []),
995
+ ...kWhens(m, sl).map((w, j) => substC(w, at(times[j])))];
996
+ const concl = substC(inline(sl.then), kStepAt(m, K, time));
997
+ // Every law at each state but the last, at the keys the proof names there.
998
+ const uses = times.slice(0, K).flatMap(u => laws.flatMap((l, i) => tuples(inst, l).map(ts => ({
999
+ node: i, subst: { 't$': u, ...Object.fromEntries(keysOf(l).map((kn, q) => [kn, substE(ts[q], at(u))])) }, guard: yes
1000
+ }))));
1001
+ const insts = [
1002
+ ...times.slice(1).map(u => ({ fact: fi(`${m.name}: a step`), at: [u] })),
1003
+ ...times.flatMap(u => state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [u] }))),
1004
+ { fact: fi(`${m.name}: params are naturals`), at: [] },
1005
+ ...(m.facts ?? []).flatMap(f => tuples(inst, f).map(ts => ({ fact: fi(f.name), at: ts.map(e => substE(e, at(times[0]))) }))),
1006
+ ];
1007
+ // Each map cell the node reads at a time after the first: that map's step there, back to the time before, and so on.
1008
+ const seen = new Set(), ix = new Map(times.map((u, j) => [JSON.stringify(u), j]));
1009
+ const cellFacts = (x) => {
1010
+ if (!x || typeof x !== 'object')
1011
+ return;
1012
+ if (Array.isArray(x)) {
1013
+ x.forEach(cellFacts);
1014
+ return;
1015
+ }
1016
+ const o = x;
1017
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && traced.includes(o.map.slice(0, -1))) {
1018
+ const key = o.key;
1019
+ if (key.op === 'add' && key.b.op === 'prod' && JSON.stringify(key.b.b) === JSON.stringify(omega)) {
1020
+ const j = ix.get(JSON.stringify(key.a)), map = o.map.slice(0, -1), id = `${map}:${JSON.stringify(key)}`, u = key.a, cell = key.b.a;
1021
+ if (j !== undefined && !seen.has(id) && mapNames.includes(map)) {
1022
+ seen.add(id);
1023
+ const more = [...(j > 0 ? ['at a step'] : []), '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [u, cell] }));
1024
+ insts.push(...more);
1025
+ for (const p of more)
1026
+ cellFacts(substC(facts[p.fact].holds, vars({ 'u$': p.at[0], 'k$': p.at[1] })));
1027
+ }
1028
+ }
1029
+ }
1030
+ for (const val of Object.values(o))
1031
+ cellFacts(val);
1032
+ };
1033
+ cellFacts([hyps, concl, uses.map(us => substC(laws[us.node].holds, { ...at(us.subst['t$']), vars: { ...at(us.subst['t$']).vars, ...Object.fromEntries(Object.entries(us.subst).filter(([n]) => n !== 't$')) } }))]);
1034
+ for (const p of [...insts])
1035
+ if (p.fact === fi(`${m.name}: a step`))
1036
+ cellFacts(substC(facts[p.fact].holds, vars({ 'u$': p.at[0] })));
1037
+ return { name: `${m.name}: ${sl.name}`, hyps, concl, rank: rank === 1 ? [t] : [t, ...Array.from({ length: rank - 1 }, () => k(2))], insts: insts.filter(p => p.fact >= 0), uses };
1038
+ });
1039
+ return sliced(withReads(facts, specs, [...lawNodes, ...specs]));
1040
+ }
1041
+ // A request allowed (BridgeRules.lean's cycAOk, cyc_allow_sound carries it to the claim): from every reachable state,
1042
+ // every request meeting `w` (a helpful request's region and terms), carrying empty sets, is one the machine allows. A node
1043
+ // at time t, the request the run takes at t the claim's (no step reads it): the precondition, t <= T, T < Ω, `w` at t,
1044
+ // each carried map's cell the assumption reads at t 0, every law at t (rank (t, 2) above theirs), the run's facts at t;
1045
+ // its conclusion the assumption at t, flags inlined.
1046
+ export function allowNode(m, laws, inst, w, facts, lawNodes, name, extra = []) {
1047
+ const { at } = trace(m), flags = flagsOf(m), t = v('t$'), T = v('T$'), mapNames = Object.keys(m.maps ?? {}), carried = m.inputMaps ?? [];
1048
+ const traced = [...mapNames, ...carried], state = Object.keys(m.state), now = at(t);
1049
+ const fi = (n) => facts.findIndex(f => f.name === n);
1050
+ const keysOf = (l) => [...(l.forall ? [l.forall] : []), ...(l.keys ?? [])];
1051
+ const tuples = (l) => { const ks = keysOf(l), flat = inst[l.name] ?? []; return ks.length ? Array.from({ length: Math.floor(flat.length / ks.length) }, (_, i) => flat.slice(i * ks.length, (i + 1) * ks.length)) : [[]]; };
1052
+ const concl = substC(substC(m.assume ?? yes, { vars: {}, maps: {}, flags }), now);
1053
+ // Each carried cell the conclusion and the facts given at t read there (a flag's body at its witness, say): 0 (the
1054
+ // request carries empty sets).
1055
+ const zeros = [], seen = new Set();
1056
+ const walk = (x) => {
1057
+ if (!x || typeof x !== 'object')
1058
+ return;
1059
+ const o = x;
1060
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && carried.includes(o.map.slice(0, -1))) {
1061
+ const id = JSON.stringify(o);
1062
+ if (!seen.has(id)) {
1063
+ seen.add(id);
1064
+ zeros.push(eq(o, k(0)));
1065
+ }
1066
+ }
1067
+ for (const val of Object.values(o))
1068
+ walk(val);
1069
+ };
1070
+ walk([concl, instHyps(facts, extra)]);
1071
+ const hyps = [...(m.pre ? [m.pre] : []), le(t, T), ...(traced.length ? [{ op: '<', a: T, b: omega }] : []), substC(w, now), ...zeros];
1072
+ const uses = laws.flatMap((l, i) => tuples(l).map(ts => ({ node: i, subst: Object.fromEntries(keysOf(l).map((kn, q) => [kn, substE(ts[q], now)])), guard: yes })));
1073
+ const insts = [{ fact: fi(`${m.name}: the start`), at: [t] }, { fact: fi(`${m.name}: a step`), at: [t] },
1074
+ ...state.map(st => ({ fact: fi(`${m.name}: ${st} is a natural`), at: [t] })), { fact: fi(`${m.name}: params are naturals`), at: [] },
1075
+ ...(m.facts ?? []).flatMap(f => tuples(f).map(ts => ({ fact: fi(f.name), at: ts.map(e => substE(e, now)) }))), ...extra];
1076
+ // Each map cell read at t: its start, its step and that it is a natural there.
1077
+ const cells = new Set();
1078
+ const cellWalk = (x) => {
1079
+ if (!x || typeof x !== 'object')
1080
+ return;
1081
+ const o = x;
1082
+ if (o.op === 'get' && typeof o.map === 'string' && o.map.endsWith('@') && mapNames.includes(o.map.slice(0, -1))) {
1083
+ const key = o.key;
1084
+ if (key.op === 'add' && JSON.stringify(key.a) === JSON.stringify(t) && key.b.op === 'prod') {
1085
+ const map = o.map.slice(0, -1), cell = key.b.a, id = `${map}:${JSON.stringify(cell)}`;
1086
+ if (!cells.has(id)) {
1087
+ cells.add(id);
1088
+ insts.push(...['at the start', 'at a step', '\'s cells are naturals'].map(what => ({ fact: fi(`${m.name}: ${map}${what.startsWith('\'') ? what : ` ${what}`}`), at: [t, cell] })));
1089
+ }
1090
+ }
1091
+ }
1092
+ for (const val of Object.values(o))
1093
+ cellWalk(val);
1094
+ };
1095
+ cellWalk([hyps, concl, uses.map(us => substC(laws[us.node].holds, { ...now, vars: { ...now.vars, ...us.subst } }))]);
1096
+ const rank = lawNodes[0]?.rank.length ?? 2;
1097
+ const spec = { name, hyps, concl, rank: [t, ...Array.from({ length: rank - 1 }, () => k(2))], insts: insts.filter(p => p.fact >= 0), uses };
1098
+ return sliced(withReads(facts, [spec], [...lawNodes, spec]))[0];
1099
+ }
1100
+ // --- flags that hold at every key ------------------------------------------------------------------------------------
1101
+ // A machine with flags that hold at every key, read as its sk machine (BridgeRules.lean's SMachine.sk): each such flag
1102
+ // ?f and its witnesses inputs a request names, a read of the flag `?f = 1`; the closing conditions facts of the run
1103
+ // (`qflagFacts`). Untrusted: BridgeRules.lean's cycQMOk checks the facts against the machine.
1104
+ const quantifiedOf = (m) => Object.entries(m.step.flags ?? {}).filter((e) => e[1].op === 'all');
1105
+ export const hasQuantified = (m) => quantifiedOf(m).length > 0;
1106
+ export function skMachine(m) {
1107
+ const q = quantifiedOf(m);
1108
+ if (!q.length)
1109
+ return m;
1110
+ const flags = Object.fromEntries(Object.entries(m.step.flags ?? {}).map(([f, c]) => [f, c.op === 'all' ? eq(v(`?${f}`), k(1)) : c]));
1111
+ return { ...m, inputs: [...m.inputs, ...q.map(([f]) => `?${f}`), ...q.flatMap(([f, c]) => witnessesOf(f, c))], step: { ...m.step, flags } };
1112
+ }
1113
+ // Each flag's closing conditions at each time u <= T (BridgeRules.lean's cycQFacts): 0 or 1; 0, or its body at every
1114
+ // value of its keys; 1, or its body failing at the witness.
1115
+ export function qflagFacts(m) {
1116
+ const { at } = trace(skMachine(m)), u = v('u$'), T = v('T$');
1117
+ const upTo = (c) => implies(le(u, T), substC(c, at(u)));
1118
+ return quantifiedOf(m).flatMap(([f, q]) => {
1119
+ const fv = v(`?${f}`), keys = boundOf(q), wits = witnessesOf(f, q);
1120
+ return [
1121
+ { name: `${m.name}: ?${f} is 0 or 1`, keys: ['u$'], holds: upTo(le(fv, k(1))) },
1122
+ { name: `${m.name}: ?${f} is 0 or its body holds`, keys: ['u$', ...keys], holds: upTo({ op: 'or', xs: [eq(fv, k(0)), q.c] }) },
1123
+ { name: `${m.name}: ?${f} is 1 or its body fails at the witness`, keys: ['u$'],
1124
+ holds: upTo({ op: 'or', xs: [eq(fv, k(1)), { op: 'not', x: substC(q.c, vars(Object.fromEntries(keys.map((key, i) => [key, v(wits[i])])))) }] }) },
1125
+ ];
1126
+ });
1127
+ }
1128
+ // The closing facts at the times a node reads the run (each flag's at its time, its body at the keys the usual proof
1129
+ // closes it at, read there).
1130
+ export function closingInsts(m, facts, insts, times, laws = []) {
1131
+ // (a law's fresh key k#, which the usual proof's keys may read, the node's own k)
1132
+ const fresh = vars(Object.fromEntries(laws.flatMap(x => (x.forall ? [[`${x.forall}#`, v(x.forall)]] : []))));
1133
+ const { at } = trace(skMachine(m)), fi = (n) => facts.findIndex(f => f.name === n), out = [], seen = new Set();
1134
+ const push = (p) => { const id = JSON.stringify(p); if (p.fact >= 0 && !seen.has(id)) {
1135
+ seen.add(id);
1136
+ out.push(p);
1137
+ } };
1138
+ for (const [f, q] of quantifiedOf(m)) {
1139
+ const n = boundOf(q).length;
1140
+ for (const u of times) {
1141
+ push({ fact: fi(`${m.name}: ?${f} is 0 or 1`), at: [u] });
1142
+ push({ fact: fi(`${m.name}: ?${f} is 1 or its body fails at the witness`), at: [u] });
1143
+ for (const inst of insts) {
1144
+ const flat = inst[`?${f}`] ?? [];
1145
+ for (let i = 0; i + n <= flat.length; i += n)
1146
+ push({ fact: fi(`${m.name}: ?${f} is 0 or its body holds`), at: [u, ...flat.slice(i, i + n).map(e => substE(substE(e, fresh), at(u)))] });
1147
+ }
1148
+ }
1149
+ }
1150
+ return out;
1151
+ }
1152
+ // The laws' nodes of a machine with such flags: its sk machine's (searched), with the closing facts, each node reading
1153
+ // them where the run's step before it and its own time are.
1154
+ export function qflagNodes(m, laws, instances) {
1155
+ const base = machineNodes(skMachine(m), laws, instances);
1156
+ if (base.why)
1157
+ return base;
1158
+ const facts = [...base.facts, ...qflagFacts(m)], t = v('t$');
1159
+ const close = closingInsts(m, facts, Object.values(instances), [t, less1(t)], laws);
1160
+ const add = (sp) => ({ ...sp, insts: [...sp.insts, ...close], ...(sp.variants ? { variants: sp.variants.map(add) } : {}) });
1161
+ return { facts, specs: base.specs.map(add) };
254
1162
  }
255
1163
  // --- folds over trees -----------------------------------------------------------------------------------------------
256
1164
  // 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,
257
1165
  // or a node (Kind[n] = 1) with children Left[n], Right[n] numbered below n, its values from theirs and its inputs.
258
- export function foldNodes(f, laws) {
1166
+ // `usual`: the usual proof's trees (kernel.ts's foldObligations), translated (`translatedFold` below), or no nodes with why not.
1167
+ export function foldNodes(f, laws, usual, st = tstats()) {
259
1168
  const n = v('n$'), N = v('N$'), names = Object.keys(f.leaf);
260
1169
  const at = (i) => vars(Object.fromEntries([...names, ...f.inputs].map(x => [x, get(`${x}@`, i)])));
261
1170
  const kind = (i) => get('Kind@', i), left = (i) => get('Left@', i), right = (i) => get('Right@', i);
@@ -271,14 +1180,71 @@ export function foldNodes(f, laws) {
271
1180
  insts: [{ fact: 0, at: [n] }, ...[n, left(n), right(n)].map(i => ({ fact: 1, at: [i] })), { fact: 2, at: [] }],
272
1181
  uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: eq(kind(n), k(1)) }))),
273
1182
  }));
274
- return { facts, specs };
1183
+ if (!stepNodesOn())
1184
+ return { facts, specs: sliced(specs) };
1185
+ // 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
1186
+ // 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
1187
+ // case gives (the usual rule's node goal, over the table's cells at its children), using every law at both children.
1188
+ const L = laws.length, hyps = [...(f.pre ? [f.pre] : []), { op: '<', a: n, b: N }], inner = eq(kind(n), k(1));
1189
+ 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))])) });
1190
+ const fromChildren = laws.map((l) => {
1191
+ const name = `${f.name}: ${l.name}, from its children`, h = [...hyps, inner];
1192
+ 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: [] }],
1193
+ 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 }] };
1194
+ });
1195
+ const atNode = laws.map((l, i) => {
1196
+ 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: [] }],
1197
+ uses: [{ node: L + i, subst: {}, guard: inner }], variants: [{ ...specs[i], rank: [n, k(1)] }] };
1198
+ const o = shared(facts, a, fromChildren[i].concl);
1199
+ return o.length ? { ...a, opaque: o } : a;
1200
+ });
1201
+ if (usual)
1202
+ return { facts, ...translatedFold() };
1203
+ return { facts, specs: sliced([...atNode, ...fromChildren]) };
1204
+ // The usual proof translated, as a machine's is: each law three nodes. At a node, as above, its goal grown: from the
1205
+ // law at a leaf (Kind[n] = 0) or from its children (Kind[n] = 1). From its children, rank (n, 0): the usual node goal
1206
+ // over the table (an input as its cell at n, l_x and r_x as x at Left[n] and Right[n]), the laws at both children its
1207
+ // uses (each child below n a hypothesis, which the table at n gives where the node is used), the assumption at n
1208
+ // too. At a leaf, rank (n, 0): the usual leaf goal at n.
1209
+ function translatedFold() {
1210
+ const goals = new Map(foldObligations(f, laws).map(g => [g.name, g])), sigma = nodeAt(n), sigma0 = at(n), ren = renamer({ s: sigma }), ren0 = renamer({ s: sigma0 });
1211
+ const na = (f.pre ? 1 : 0) + (f.assume ? 1 : 0), last = (g) => g.hyps.at(-1).x;
1212
+ const kids = [], leaves = [], made = [];
1213
+ for (const [i, l] of laws.entries()) {
1214
+ const g0 = goals.get(`${l.name} @leaf`), g1 = goals.get(`${l.name} @node`), t0 = g0 && usual[g0.name], t1 = g1 && usual[g1.name];
1215
+ if (!g0 || !g1 || !t0 || !t1 || g0.hyps.length !== na + 1 || g1.hyps.length !== na + 2 * L + 1)
1216
+ return { specs: [], why: `${l.name}: no usual tree, or one of another shape` };
1217
+ const used1 = usedHyps(g1, t1);
1218
+ kids.push({ name: `${f.name}: ${l.name}, from its children`, hyps: [...hyps, inner, { op: '<', a: left(n), b: n }, { op: '<', a: right(n), b: n }, ...(f.assume ? [substC(f.assume, sigma0)] : [])], concl: substC(last(g1), sigma), rank: [n, k(0)], insts: [],
1219
+ uses: laws.flatMap((_, j) => [left, right].map(c => ({ node: j, subst: { 'n$': c(n) }, guard: yes }))).filter((_, q) => used1.has(na + q)) });
1220
+ leaves.push({ name: `${f.name}: ${l.name}, at a leaf`, hyps: [...hyps, eq(kind(n), k(0)), ...(f.assume ? [substC(f.assume, sigma0)] : [])], concl: substC(last(g0), sigma0), rank: [n, k(0)], insts: [], uses: [] });
1221
+ made.push({ g: g1, tree: t1, node: L + i, ren }, { g: g0, tree: t0, node: 2 * L + i, ren: ren0 });
1222
+ }
1223
+ const carried = atNode.map((a, i) => {
1224
+ const b = { ...a, variants: undefined, carry: true, uses: [...a.uses, { node: 2 * L + i, subst: {}, guard: eq(kind(n), k(0)) }] };
1225
+ const o = [...new Set([...shared(facts, b, kids[i].concl), ...shared(facts, b, leaves[i].concl)].map(e => JSON.stringify(e)))].map(e => JSON.parse(e));
1226
+ return { ...b, opaque: o.length ? o : undefined };
1227
+ });
1228
+ const all = [...carried, ...kids, ...leaves], until = performance.now() + 10_000;
1229
+ for (const m of made) {
1230
+ const tree = timedKind('translate', 0, () => withProducts(() => translate(m.g, mainGoal(facts, all, all[m.node]), m.tree, m.ren, st, until)));
1231
+ if (!tree)
1232
+ return { specs: [], why: `${all[m.node].name}: not translated` };
1233
+ all[m.node] = { ...all[m.node], main: tree };
1234
+ }
1235
+ return { specs: sliced(all) };
1236
+ }
275
1237
  }
276
1238
  // --- the cyclic route beside the usual one (GDP_CYCLIC=1) -------------------------------------------------------------
277
1239
  // 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: {...}`.
1240
+ // 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
1241
  const env = () => globalThis.process?.env;
280
1242
  let quiet = 0;
281
1243
  export const cyclicOn = () => quiet === 0 && env()?.GDP_CYCLIC === '1';
1244
+ // The `cyclic: {...}` lines research/cyclic/ratios.ts reads, printed only with GDP_CYCLIC=1 (the cross-check) or
1245
+ // GDP_CYCLIC_LOG=1 (modal/run.py sets it): a user's proofs print nothing.
1246
+ export const cycLog = (o) => { if (env()?.GDP_CYCLIC === '1' || env()?.GDP_CYCLIC_LOG === '1')
1247
+ console.error(`cyclic: ${JSON.stringify(o)}`); };
282
1248
  // A search's many trial proofs (core/discover.ts, core/infer.ts) aren't compared: what it finds is proven again.
283
1249
  export const quietly = (f) => { quiet++; try {
284
1250
  return f();
@@ -286,10 +1252,130 @@ export const quietly = (f) => { quiet++; try {
286
1252
  finally {
287
1253
  quiet--;
288
1254
  } };
289
- export function crossCyclic(kind, name, usual, run, usualMs) {
290
- const t0 = performance.now(), out = run(), ms = Math.round(performance.now() - t0);
291
- const cyclic = out.proven && out.checked;
292
- console.error(`cyclic: ${JSON.stringify({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) })}`);
293
- if (cyclic !== usual)
1255
+ // A translated proof by what it was made from (the machine, its laws and instances; a recurrence's or fold's case): a
1256
+ // proof made again (a rule proven twice, as the suite does) is the one made and checked before (the same proof: the
1257
+ // kernel's check is a function of it, and nothing changes a proof once made). The key's time is GDP_CYCLIC_PROF's `key`.
1258
+ const proofsMade = new Map();
1259
+ // `base`: the key of a proof made before whose nodes these begin with (a machine's laws, for its step laws), reused where
1260
+ // its facts are these.
1261
+ export function translatedProof(key, build, st, base) {
1262
+ const k0 = timedKind('key', 0, key), had = proofsMade.get(k0);
1263
+ if (had) {
1264
+ st.cached++;
1265
+ return { verdict: { proven: true, checked: true, proof: had } };
1266
+ }
1267
+ const b = base && proofsMade.get(timedKind('key', 0, base)), { facts, specs, why } = build(!!b);
1268
+ if (why)
1269
+ return { why };
1270
+ const made = b && b.nodes.length <= specs.length && JSON.stringify(b.facts) === JSON.stringify(facts) ? b.nodes : [];
1271
+ if (b && !made.length)
1272
+ return translatedProof(key, () => build(false), st);
1273
+ const v = proveCyclic(facts, specs, undefined, translatedCache, st, made);
1274
+ if (v.proven && v.checked) {
1275
+ if (proofsMade.size >= 1024)
1276
+ proofsMade.clear();
1277
+ proofsMade.set(k0, v.proof);
1278
+ }
1279
+ return { verdict: v };
1280
+ }
1281
+ export const routeOf = () => env()?.GDP_CYCLIC_ROUTE ?? 'translate';
1282
+ const none = () => ({ proven: false, checked: false, node: '', goal: 'no instance searched', stuck: 'unknown' });
1283
+ export function crossCyclic(kind, name, usual, run0, usualMs, translated) {
1284
+ const run = run0 ?? none;
1285
+ for (const key of Object.keys(cyclicProf))
1286
+ delete cyclicProf[key];
1287
+ const route = routeOf(), st = tstats(), t0 = performance.now();
1288
+ let out, how = 'searched', why;
1289
+ if (route !== 'search' && usual && translated) {
1290
+ const r = translated(st);
1291
+ if (r.verdict?.proven && r.verdict.checked) {
1292
+ out = r.verdict;
1293
+ how = 'translated';
1294
+ }
1295
+ else {
1296
+ how = 'fallback';
1297
+ why = r.why ?? (r.verdict && !r.verdict.proven ? `${r.verdict.goal}: ${r.verdict.stuck}` : r.verdict?.why);
1298
+ }
1299
+ }
1300
+ out ??= run();
1301
+ const ms = Math.round(performance.now() - t0), cyclic = out.proven && out.checked;
1302
+ 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)]))])) } : {};
1303
+ let search;
1304
+ if (route === 'both' && run0) {
1305
+ const s0 = performance.now(), o = run0();
1306
+ search = { searchMs: Math.round(performance.now() - s0), searchCyclic: o.proven && o.checked };
1307
+ }
1308
+ cycLog({ kind, name, usual, cyclic, ms, ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), route: how, ...(how !== 'searched' ? st : {}), ...(why ? { fellBack: why } : {}), ...search, ...prof, ...(out.proven ? { nodes: out.proof.nodes.length, ...(out.why ? { why: out.why } : {}) } : { goal: out.goal, stuck: out.stuck }) });
1309
+ const lax = env()?.GDP_CYCLIC_LAX === '1';
1310
+ if (cyclic !== usual && !lax)
294
1311
  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})`}`);
1312
+ if (search && search.searchCyclic !== usual && !lax)
1313
+ throw new Error(`cyclic route: ${kind} ${name} is ${search.searchCyclic ? 'proven' : 'not proven'} as a cyclic proof searched, ${usual ? 'proven' : 'not proven'} the usual way`);
1314
+ }
1315
+ // The certificate of every proof the instances above cover is the cyclic one (GDP_CERT=usual: the usual trees, as
1316
+ // before 0.19.0), made from the usual proof (translated, searched where that fails) and checked by kernel/cyclic.ts; the
1317
+ // usual trees are then the translation's input, not checked again (kernel/prove.ts, core/dsl.ts). Each logs a line
1318
+ // `cyclic: {..., cert: true}` (cycLog).
1319
+ export const certOn = () => quiet === 0 && env()?.GDP_CERT !== 'usual';
1320
+ // With GDP_CERT=cyclic, what no translation covers (flags that hold at every key, k-step laws over a map) gets a searched
1321
+ // certificate too where GDP_CERT_SEARCH=1, each within GDP_CERT_MS (10 s by default), and keeps the usual one where none
1322
+ // is found then. Off by default: a search costs seconds a proof.
1323
+ export const certSearch = () => env()?.GDP_CERT_SEARCH === '1';
1324
+ export const certMs = () => Number(env()?.GDP_CERT_MS ?? 10_000);
1325
+ export function certCyclic(kind, name, translated, run0, usualMs) {
1326
+ const run = run0 ?? none;
1327
+ const st = tstats(), t0 = performance.now();
1328
+ let out, how = 'searched', why;
1329
+ if (translated) {
1330
+ const r = translated(st);
1331
+ if (r.verdict?.proven && r.verdict.checked) {
1332
+ out = r.verdict;
1333
+ how = 'translated';
1334
+ }
1335
+ else {
1336
+ how = 'fallback';
1337
+ why = r.why ?? (r.verdict && !r.verdict.proven ? `${r.verdict.goal}: ${r.verdict.stuck}` : r.verdict?.why);
1338
+ }
1339
+ }
1340
+ out ??= run();
1341
+ const checked = out.proven && out.checked;
1342
+ cycLog({ kind, name, cert: true, usual: true, cyclic: checked, ms: Math.round(performance.now() - t0), ...(usualMs === undefined ? {} : { usualMs: Math.round(usualMs) }), route: how, ...(how !== 'searched' ? st : {}), ...(why ? { fellBack: why } : {}) });
1343
+ return { checked, ...(out.proven ? { proof: out.proof } : {}) };
1344
+ }
1345
+ // The step laws' searched nodes after the laws' (stepLawNodes over the sk machine), with the flags' closing facts read
1346
+ // where each node reads the run on a machine with flags that hold at every key: what GDP_CERT=cyclic's search and the
1347
+ // Lean export's (lean-kernel/emit.ts) both try.
1348
+ export function searchedStepNodes(m, laws, steps, facts, lawNodes) {
1349
+ const q = hasQuantified(m);
1350
+ return stepLawNodes(skMachine(m), laws, steps, facts, lawNodes).map((sp, i) => {
1351
+ if (!q)
1352
+ return sp;
1353
+ const more = closingInsts(m, facts, [steps[i].instances], Array.from({ length: (steps[i].law.steps ?? 1) + 1 }, (_, j) => tBack(j)));
1354
+ const add = (x) => ({ ...x, insts: [...x.insts, ...more], ...(x.variants ? { variants: x.variants.map(add) } : {}) });
1355
+ return add(sp);
1356
+ });
1357
+ }
1358
+ // Live.lean's reqC: each input named by its term, two parts per and.
1359
+ export const reqTS = (req) => (req.length ? { op: 'and', xs: [{ op: '=', a: { op: 'var', name: req[0][0] }, b: req[0][1] }, reqTS(req.slice(1))] } : { op: '<=', a: { op: 'const', value: 0 }, b: { op: 'const', value: 0 } });
1360
+ // Witnesses for a helpful request's free inputs, for its allowed goal: all free first, then each free input fixed to 0, a
1361
+ // state variable or a param (a history's query, say, which its relations bound), the first combination that proves.
1362
+ export function withWitness(m, request, attempt) {
1363
+ const free = m.inputs.filter(i => !(i in request));
1364
+ const cands = [undefined, { op: 'const', value: 0 }, ...Object.keys(m.state).map(v => ({ op: 'var', name: v })), ...m.params.map(v => ({ op: 'var', name: v }))];
1365
+ const combos = [[]];
1366
+ if (free.length <= 2) {
1367
+ const go = (i, acc) => { if (i === free.length) {
1368
+ if (acc.length)
1369
+ combos.push(acc);
1370
+ return;
1371
+ } for (const c of cands)
1372
+ go(i + 1, c ? [...acc, [free[i], c]] : acc); };
1373
+ go(0, []);
1374
+ }
1375
+ for (const wit of combos) {
1376
+ const out = attempt(wit);
1377
+ if (out !== undefined)
1378
+ return { wit, out };
1379
+ }
1380
+ return undefined;
295
1381
  }