gdp-wired 0.17.0 → 0.18.0

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